diff --git a/CODEOWNERS b/CODEOWNERS index 7c7b6ee9..4c1260f6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -69,6 +69,15 @@ /src/CommunityToolkit.Aspire.Hosting.Dapr**/ @FullStackChef @WhitWaldo @Paule96 /tests/CommunityToolkit.Aspire.Hosting.Dapr**.Tests/ @FullStackChef @WhitWaldo @Paule96 +# CommunityToolkit.Aspire.RavenDB.Client +# CommunityToolkit.Aspire.Hosting.RavenDB + +/examples/ravendb/ @shiranshalom +/src/CommunityToolkit.Aspire.RavenDB.Client/ @shiranshalom +/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/ @shiranshalom +/src/CommunityToolkit.Aspire.Hosting.RavenDB/ @shiranshalom +/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/ @shiranshalom + # CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions /src/CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions/ @Alirexaa diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index ac787e54..6b2c05c3 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -265,6 +265,21 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hos EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults", "examples\dapr\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults\CommunityToolkit.Aspire.Hosting.Dapr.ServiceDefaults.csproj", "{99441705-4BFA-499F-9897-371238665E38}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.RavenDB.Client", "src\CommunityToolkit.Aspire.RavenDB.Client\CommunityToolkit.Aspire.RavenDB.Client.csproj", "{11768120-E86C-4464-A68A-6F9BD0999BB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.RavenDB.Client.Tests", "tests\CommunityToolkit.Aspire.RavenDB.Client.Tests\CommunityToolkit.Aspire.RavenDB.Client.Tests.csproj", "{BA416CE7-0C29-4FBA-B31A-327A7ECB56C9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Hosting.RavenDB", "src\CommunityToolkit.Aspire.Hosting.RavenDB\CommunityToolkit.Aspire.Hosting.RavenDB.csproj", "{A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Aspire.Hosting.RavenDB.Tests", "tests\CommunityToolkit.Aspire.Hosting.RavenDB.Tests\CommunityToolkit.Aspire.Hosting.RavenDB.Tests.csproj", "{35B51242-F576-4CCE-BA29-712A80749CB1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ravendb", "ravendb", "{1C65F967-D5F1-424B-82E9-B8585B6F0BD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.RavenDB.AppHost", "examples\ravendb\RavenDB.AppHost\CommunityToolkit.Aspire.Hosting.RavenDB.AppHost.csproj", "{28FCB1E2-7460-4FDD-AA63-03162C9F2154}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults", "examples\ravendb\CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults\CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults.csproj", "{3D076CFF-6482-4126-9F29-C7617E7D2F5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.RavenDB.ApiService", "examples\ravendb\CommunityToolkit.Aspire.Hosting.RavenDB.ApiService\CommunityToolkit.Aspire.Hosting.RavenDB.ApiService.csproj", "{D214CBF5-D5E4-4641-868D-66B0C5337DD5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.DbGate.Tests", "tests\CommunityToolkit.Aspire.Hosting.DbGate.Tests\CommunityToolkit.Aspire.Hosting.DbGate.Tests.csproj", "{BDAF7D27-C600-4419-9782-CF15BA5272E9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postgres-ext", "postgres-ext", "{204BB8D8-04E3-4FE5-BB08-E793BF532F2F}" @@ -721,6 +736,34 @@ Global {99441705-4BFA-499F-9897-371238665E38}.Debug|Any CPU.Build.0 = Debug|Any CPU {99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.ActiveCfg = Release|Any CPU {99441705-4BFA-499F-9897-371238665E38}.Release|Any CPU.Build.0 = Release|Any CPU + {11768120-E86C-4464-A68A-6F9BD0999BB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11768120-E86C-4464-A68A-6F9BD0999BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11768120-E86C-4464-A68A-6F9BD0999BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11768120-E86C-4464-A68A-6F9BD0999BB9}.Release|Any CPU.Build.0 = Release|Any CPU + {BA416CE7-0C29-4FBA-B31A-327A7ECB56C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA416CE7-0C29-4FBA-B31A-327A7ECB56C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA416CE7-0C29-4FBA-B31A-327A7ECB56C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA416CE7-0C29-4FBA-B31A-327A7ECB56C9}.Release|Any CPU.Build.0 = Release|Any CPU + {A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A}.Release|Any CPU.Build.0 = Release|Any CPU + {35B51242-F576-4CCE-BA29-712A80749CB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35B51242-F576-4CCE-BA29-712A80749CB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35B51242-F576-4CCE-BA29-712A80749CB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35B51242-F576-4CCE-BA29-712A80749CB1}.Release|Any CPU.Build.0 = Release|Any CPU + {28FCB1E2-7460-4FDD-AA63-03162C9F2154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28FCB1E2-7460-4FDD-AA63-03162C9F2154}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28FCB1E2-7460-4FDD-AA63-03162C9F2154}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28FCB1E2-7460-4FDD-AA63-03162C9F2154}.Release|Any CPU.Build.0 = Release|Any CPU + {3D076CFF-6482-4126-9F29-C7617E7D2F5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D076CFF-6482-4126-9F29-C7617E7D2F5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D076CFF-6482-4126-9F29-C7617E7D2F5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D076CFF-6482-4126-9F29-C7617E7D2F5B}.Release|Any CPU.Build.0 = Release|Any CPU + {D214CBF5-D5E4-4641-868D-66B0C5337DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D214CBF5-D5E4-4641-868D-66B0C5337DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D214CBF5-D5E4-4641-868D-66B0C5337DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D214CBF5-D5E4-4641-868D-66B0C5337DD5}.Release|Any CPU.Build.0 = Release|Any CPU {BDAF7D27-C600-4419-9782-CF15BA5272E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BDAF7D27-C600-4419-9782-CF15BA5272E9}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDAF7D27-C600-4419-9782-CF15BA5272E9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -890,6 +933,14 @@ Global {D2DDEA96-4A7E-496B-AFBE-69A133156C5F} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} {5ADBE907-7E0B-4AD7-9073-C032C4183914} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} {99441705-4BFA-499F-9897-371238665E38} = {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} + {11768120-E86C-4464-A68A-6F9BD0999BB9} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {BA416CE7-0C29-4FBA-B31A-327A7ECB56C9} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {A7852D8B-BE38-4D95-A8D6-3B6F96F94A5A} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {35B51242-F576-4CCE-BA29-712A80749CB1} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {1C65F967-D5F1-424B-82E9-B8585B6F0BD6} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {28FCB1E2-7460-4FDD-AA63-03162C9F2154} = {1C65F967-D5F1-424B-82E9-B8585B6F0BD6} + {3D076CFF-6482-4126-9F29-C7617E7D2F5B} = {1C65F967-D5F1-424B-82E9-B8585B6F0BD6} + {D214CBF5-D5E4-4641-868D-66B0C5337DD5} = {1C65F967-D5F1-424B-82E9-B8585B6F0BD6} {BDAF7D27-C600-4419-9782-CF15BA5272E9} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {204BB8D8-04E3-4FE5-BB08-E793BF532F2F} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {356853EE-2C47-429C-B6CF-F3F76B6FFD91} = {899F0713-7FC6-4750-BAFC-AC650B35B453} diff --git a/Directory.Packages.props b/Directory.Packages.props index f440a813..fab4ba51 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,6 +69,9 @@ + + + diff --git a/README.md b/README.md index 34421955..1ab168f8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. | | - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. | | - **Learn More**: [`Hosting.Dapr.AzureRedis`][dapr-azureredis-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. | +| - **Learn More**: [`Hosting.RavenDB`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields]][ravendb-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields-preview]][ravendb-nuget-preview] | An Aspire integration leveraging the [RavenDB](https://ravendb.net/) container. | +| - **Learn More**: [`RavenDB.Client`][ravendb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields]][ravendb-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields-preview]][ravendb-client-nuget-preview] | An Aspire client integration for the [RavenDB.Client](https://www.nuget.org/packages/RavenDB.client) package. | | - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. | | - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. | @@ -184,6 +186,15 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [dapr-azureredis-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/ [dapr-azureredis-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis?label=nuget%20(preview) [dapr-azureredis-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr.AzureRedis/absoluteLatest +[ravendb-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/ravendb +[ravendb-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.RavenDB +[ravendb-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.RavenDB/ +[ravendb-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.RavenDB?label=nuget%20(preview) +[ravendb-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.RavenDB/absoluteLatest +[ravendb-client-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.RavenDB.Client +[ravendb-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.RavenDB.Client/ +[ravendb-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.RavenDB.Client?label=nuget%20(preview) +[ravendb-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.RavenDB.Client/absoluteLatest [go-feature-flag-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-go-feature-flag [go-feature-flag-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.GoFeatureFlag [go-feature-flag-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/ diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService.csproj b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService.csproj new file mode 100644 index 00000000..e1650205 --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService.csproj @@ -0,0 +1,13 @@ + + + + enable + enable + + + + + + + + diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Program.cs b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Program.cs new file mode 100644 index 00000000..3827cde5 --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Program.cs @@ -0,0 +1,46 @@ +using Raven.Client.Documents; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRavenDBClient(connectionName: "ravendb", configureSettings: settings => +{ + settings.CreateDatabase = true; + settings.DatabaseName = "ravenDatabase"; +}); + +var app = builder.Build(); + +app.MapGet("/create", async (IDocumentStore documentStore) => +{ + using var session = documentStore.OpenAsyncSession(); + var company = new Company + { + Name = "RavenDB", + Phone = "(26) 642-7012", + Fax = "(26) 642-7012" + }; + + await session.StoreAsync(company, "companies/ravendb"); + await session.SaveChangesAsync(); +}); + +app.MapGet("/get", async (IDocumentStore documentStore) => +{ + using var session = documentStore.OpenAsyncSession(); + var company = await session.LoadAsync("companies/ravendb"); + return company; +}); + +app.MapDefaultEndpoints(); + +app.Run(); + + +public class Company +{ + public string? Id { get; set; } + public string? Name { get; set; } + public string? Phone { get; set; } + public string? Fax { get; set; } +} diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Properties/launchSettings.json b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Properties/launchSettings.json new file mode 100644 index 00000000..4528202f --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "CommunityToolkit.Aspire.Hosting.RavenDB.ApiService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:54649" + } + } +} \ No newline at end of file diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/RavenDB.ApiService.http b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/RavenDB.ApiService.http new file mode 100644 index 00000000..e04f558d --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ApiService/RavenDB.ApiService.http @@ -0,0 +1,6 @@ +@ApiService_HostAddress = http://localhost:54649 + +GET {{ApiService_HostAddress}}/ +Accept: application/json + +### diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults.csproj b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults.csproj new file mode 100644 index 00000000..c9a4399a --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/Extensions.cs b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..9b608dc4 --- /dev/null +++ b/examples/ravendb/CommunityToolkit.Aspire.Hosting.RavenDB.ServiceDefaults/Extensions.cs @@ -0,0 +1,116 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/examples/ravendb/RavenDB.AppHost/CommunityToolkit.Aspire.Hosting.RavenDB.AppHost.csproj b/examples/ravendb/RavenDB.AppHost/CommunityToolkit.Aspire.Hosting.RavenDB.AppHost.csproj new file mode 100644 index 00000000..a2481892 --- /dev/null +++ b/examples/ravendb/RavenDB.AppHost/CommunityToolkit.Aspire.Hosting.RavenDB.AppHost.csproj @@ -0,0 +1,22 @@ + + + + + Exe + enable + enable + true + f39fb70f-21f3-4af9-89b4-3062ff4431e6 + false + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/ravendb/RavenDB.AppHost/Program.cs b/examples/ravendb/RavenDB.AppHost/Program.cs new file mode 100644 index 00000000..0a5c4689 --- /dev/null +++ b/examples/ravendb/RavenDB.AppHost/Program.cs @@ -0,0 +1,14 @@ +using CommunityToolkit.Aspire.Hosting.RavenDB; +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var serverSettings = RavenDBServerSettings.Unsecured(); +var ravendb = builder.AddRavenDB("ravendb", serverSettings); +ravendb.AddDatabase("ravenDatabase"); + +builder.AddProject("apiservice") + .WithReference(ravendb) + .WaitFor(ravendb); + +builder.Build().Run(); diff --git a/examples/ravendb/RavenDB.AppHost/Properties/launchSettings.json b/examples/ravendb/RavenDB.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..b2707245 --- /dev/null +++ b/examples/ravendb/RavenDB.AppHost/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "RavenDB.AppHost": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "\"\"", + "DISABLE_OTLP": "true", + "ASPNETCORE_URLS": "http://localhost:5000", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/CommunityToolkit.Aspire.Hosting.RavenDB.csproj b/src/CommunityToolkit.Aspire.Hosting.RavenDB/CommunityToolkit.Aspire.Hosting.RavenDB.csproj new file mode 100644 index 00000000..3b23c520 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/CommunityToolkit.Aspire.Hosting.RavenDB.csproj @@ -0,0 +1,17 @@ + + + + An Aspire integration leveraging the RavenDB container. + hosting ravendb + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/HealthChecksExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/HealthChecksExtensions.cs new file mode 100644 index 00000000..049ec3b7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/HealthChecksExtensions.cs @@ -0,0 +1,111 @@ +using HealthChecks.RavenDB; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Data.Common; + +namespace CommunityToolkit.Aspire.Hosting.RavenDB; + +internal static class HealthChecksExtensions +{ + private const string NAME = "ravendb"; + + /// + /// Add a health check for RavenDB server services. + /// + /// The . + /// A factory to build the connection string to use. + /// The health check name. Optional. If null the type name 'ravendb' will be used for the name. + /// that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRavenDB( + this IHealthChecksBuilder builder, + Func connectionStringFactory, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => + { + var connectionString = ValidateConnectionString(connectionStringFactory, sp); + return new RavenDBHealthCheck(new RavenDBOptions { Urls = new[] { connectionString } }); + }, + failureStatus, + tags, + timeout)); + } + + /// + /// Add a health check for RavenDB database services. + /// + /// The . + /// A factory to build the connection string to use. + /// The database name to check. + /// The health check name. Optional. If null the type name 'ravendb' will be used for the name. + /// that should be reported when the health check fails. Optional. If null then + /// the default status of will be reported. + /// A list of tags that can be used to filter sets of health checks. Optional. + /// An optional representing the timeout of the check. + /// The specified . + public static IHealthChecksBuilder AddRavenDB( + this IHealthChecksBuilder builder, + Func connectionStringFactory, + string databaseName, + string? name = default, + HealthStatus? failureStatus = default, + IEnumerable? tags = default, + TimeSpan? timeout = default) + { + return builder.Add(new HealthCheckRegistration( + name ?? NAME, + sp => + { + var connectionString = ValidateConnectionString(connectionStringFactory, sp); + return new RavenDBHealthCheck(new RavenDBOptions + { + Urls = new[] { connectionString }, + Database = databaseName + }); + }, + failureStatus, + tags, + timeout)); + } + + /// + /// Validates that the connection string is not null or empty. + /// + /// The factory to generate the connection string. + /// The service provider instance. + /// A valid, non-empty connection string. + /// Thrown if the connection string is null or empty. + private static string ValidateConnectionString(Func connectionStringFactory, IServiceProvider serviceProvider) + { + var connectionString = connectionStringFactory(serviceProvider); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException("Failed to generate a valid RavenDB connection string. The result cannot be null or empty."); + } + + var connectionBuilder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (connectionBuilder.TryGetValue("URL", out var url) && url is string serverUrl) + { + connectionString = serverUrl; + } + else + { + throw new InvalidOperationException("Connection string is unavailable"); + } + + return connectionString; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..1b06c631 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/PublicAPI.Unshipped.txt @@ -0,0 +1,42 @@ +#nullable enable +Aspire.Hosting.ApplicationModel.RavenDBServerResource +Aspire.Hosting.ApplicationModel.RavenDBServerResource.RavenDBServerResource(string! name, bool isSecured) -> void +Aspire.Hosting.ApplicationModel.RavenDBServerResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference! +Aspire.Hosting.ApplicationModel.RavenDBServerResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +Aspire.Hosting.ApplicationModel.RavenDBServerResource.Databases.get -> System.Collections.Generic.IReadOnlyDictionary! +Aspire.Hosting.RavenDBBuilderExtensions +static Aspire.Hosting.RavenDBBuilderExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, string? databaseName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RavenDBBuilderExtensions.AddRavenDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RavenDBBuilderExtensions.AddRavenDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, bool secured, System.Collections.Generic.Dictionary! environmentVariables, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RavenDBBuilderExtensions.AddRavenDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings! serverSettings) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RavenDBBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.RavenDBBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +Aspire.Hosting.ApplicationModel.RavenDBDatabaseResource +Aspire.Hosting.ApplicationModel.RavenDBDatabaseResource.RavenDBDatabaseResource(string! name, string! databaseName, Aspire.Hosting.ApplicationModel.RavenDBServerResource! parent) -> void +Aspire.Hosting.ApplicationModel.RavenDBDatabaseResource.Parent.get -> Aspire.Hosting.ApplicationModel.RavenDBServerResource! +Aspire.Hosting.ApplicationModel.RavenDBDatabaseResource.DatabaseName.get -> string! +Aspire.Hosting.ApplicationModel.RavenDBDatabaseResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBSecuredServerSettings +CommunityToolkit.Aspire.Hosting.RavenDB.LicensingOptions +CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode.None = 0 -> CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode.LetsEncrypt = 1 -> CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode.Secured = 2 -> CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode.Unsecured = 3 -> CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.RavenDBServerSettings() -> void +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.ServerUrl.get -> string? +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.ServerUrl.set -> void +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.SetupMode.get -> CommunityToolkit.Aspire.Hosting.RavenDB.SetupMode +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.LicensingOptions.get -> CommunityToolkit.Aspire.Hosting.RavenDB.LicensingOptions? +static CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.Secured(string! domainUrl, string! certificatePath, string? certificatePassword = null, string? serverUrl = null) -> CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings! +static CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.SecuredWithLetsEncrypt(string! domainUrl, string! certificatePath, string? certificatePassword = null, string? serverUrl = null) -> CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings! +static CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.Unsecured() -> CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings! +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBServerSettings.WithLicense(string! license, bool eulaAccepted = true) -> void +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBSecuredServerSettings.RavenDBSecuredServerSettings(string! certificatePath, string? certificatePassword, string! publicServerUrl) -> void +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBSecuredServerSettings.CertificatePath.get -> string! +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBSecuredServerSettings.CertificatePassword.get -> string? +CommunityToolkit.Aspire.Hosting.RavenDB.RavenDBSecuredServerSettings.PublicServerUrl.get -> string! +CommunityToolkit.Aspire.Hosting.RavenDB.LicensingOptions.LicensingOptions(string! license, bool eulaAccepted = true) -> void +CommunityToolkit.Aspire.Hosting.RavenDB.LicensingOptions.License.get -> string! +CommunityToolkit.Aspire.Hosting.RavenDB.LicensingOptions.EulaAccepted.get -> bool \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/README.md b/src/CommunityToolkit.Aspire.Hosting.RavenDB/README.md new file mode 100644 index 00000000..cab29840 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/README.md @@ -0,0 +1,33 @@ +# CommunityToolkit.Aspire.Hosting.RavenDB library + +Provides extension methods and resource definitions for a .NET Aspire AppHost to configure a RavenDB resource. + +## Getting started + +### Install the package + +In your AppHost project, install the .NET Aspire RavenDB Hosting library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.RavenDB +``` + +## Usage example + +Then, in the _Program.cs_ file of `AppHost`, add a RavenDB resource and consume the connection using the following methods: + +```csharp +var db = builder.AddRavenDB("ravendb").AddDatabase("mydb"); + +var myService = builder.AddProject() + .WithReference(db); +``` + +## Additional documentation + + +https://learn.microsoft.com/dotnet/aspire/community-toolkit/ravendb + +## Feedback & contributing + +https://github.com/CommunityToolkit/Aspire diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs new file mode 100644 index 00000000..45bc8b71 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBBuilderExtensions.cs @@ -0,0 +1,219 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using CommunityToolkit.Aspire.Hosting.RavenDB; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding RavenDB resources to an . +/// +public static class RavenDBBuilderExtensions +{ + /// + /// Adds a RavenDB server resource to the application model. A container is used for local development. + /// This overload simplifies the configuration by creating an unsecured RavenDB server resource with default settings. + /// + /// + /// + /// Note: When using this method, a valid RavenDB license must be provided as an environment variable + /// before calling the and methods. + /// You can set the license by calling: + /// + /// builder.WithEnvironment("RAVEN_License", "{your license}"); + /// + /// + /// + /// The to which the resource is added. + /// The name of the RavenDB server resource. + /// A resource builder for the newly added RavenDB server resource. + /// Thrown if is null. + public static IResourceBuilder AddRavenDB(this IDistributedApplicationBuilder builder, [ResourceName] string name) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.AddRavenDB(name, RavenDBServerSettings.Unsecured()); + } + + /// + /// Adds a RavenDB server resource to the application model. A container is used for local development. + /// This version of the package defaults to the tag of the container image. + /// This overload simplifies configuration by accepting a object to specify server settings. + /// + /// The + /// The name of the RavenDB server resource. + /// An object of type containing configuration details for the RavenDB server, + /// such as whether the server should use HTTPS, RavenDB license and other relevant settings. + /// A resource builder for the newly added RavenDB server resource. + /// Thrown when the connection string cannot be retrieved during configuration. + /// Thrown when the connection string is unavailable. + public static IResourceBuilder AddRavenDB(this IDistributedApplicationBuilder builder, + [ResourceName] string name, + RavenDBServerSettings serverSettings) + { + ArgumentNullException.ThrowIfNull(builder); + + var environmentVariables = GetEnvironmentVariablesFromServerSettings(serverSettings); + return builder.AddRavenDB(name, secured: serverSettings is RavenDBSecuredServerSettings, environmentVariables); + } + + /// + /// Adds a RavenDB server resource to the application model. A container is used for local development. + /// This version of the package defaults to the tag of the container image. + /// + /// The + /// The name of the RavenDB server resource. + /// Indicates whether the server connection should be secured (HTTPS). Defaults to false. + /// The environment variables to configure the RavenDB server. + /// Optional port for the server. If not provided, defaults to the container's internal port (8080). + /// A resource builder for the newly added RavenDB server resource. + /// Thrown when the connection string cannot be retrieved during configuration. + /// Thrown when the connection string is unavailable. + public static IResourceBuilder AddRavenDB(this IDistributedApplicationBuilder builder, + [ResourceName] string name, + bool secured, + Dictionary environmentVariables, + int? port = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + var serverResource = new RavenDBServerResource(name, secured); + + string? connectionString = null; + builder.Eventing.Subscribe(serverResource, async (@event, ct) => + { + connectionString = await serverResource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + + if (connectionString is null) + throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{serverResource.Name}' resource but the connection string was null."); + }); + + var healthCheckKey = $"{name}_check"; + builder.Services.AddHealthChecks() + .AddRavenDB(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), + name: healthCheckKey); + + return builder + .AddResource(serverResource) + .WithEndpoint(port: port, targetPort: secured ? 443 : 8080, scheme: serverResource.PrimaryEndpointName, name: serverResource.PrimaryEndpointName, isProxied: false) + .WithImage(RavenDBContainerImageTags.Image, RavenDBContainerImageTags.Tag) + .WithImageRegistry(RavenDBContainerImageTags.Registry) + .WithEnvironment(context => ConfigureEnvironmentVariables(context, environmentVariables)) + .WithHealthCheck(healthCheckKey); + } + + private static Dictionary GetEnvironmentVariablesFromServerSettings(RavenDBServerSettings serverSettings) + { + var environmentVariables = new Dictionary + { + { "RAVEN_Setup_Mode", serverSettings.SetupMode.ToString() } + }; + + if (serverSettings.LicensingOptions is not null) + { + environmentVariables.TryAdd("RAVEN_License_Eula_Accepted", serverSettings.LicensingOptions.EulaAccepted.ToString()); + environmentVariables.TryAdd("RAVEN_License", serverSettings.LicensingOptions.License); + } + + if (serverSettings.ServerUrl is not null) + environmentVariables.TryAdd("RAVEN_ServerUrl", serverSettings.ServerUrl); + + if (serverSettings is RavenDBSecuredServerSettings securedServerSettings) + { + environmentVariables.TryAdd("RAVEN_PublicServerUrl", securedServerSettings.PublicServerUrl); + environmentVariables.TryAdd("RAVEN_Security_Certificate_Path", securedServerSettings.CertificatePath); + + if (securedServerSettings.CertificatePassword is not null) + environmentVariables.TryAdd("RAVEN_Security_Certificate_Password", securedServerSettings.CertificatePassword); + } + + return environmentVariables; + } + + private static void ConfigureEnvironmentVariables(EnvironmentCallbackContext context, Dictionary? environmentVariables = null) + { + if (environmentVariables is null) + { + context.EnvironmentVariables.TryAdd("RAVEN_Setup_Mode", "None"); + context.EnvironmentVariables.TryAdd("RAVEN_Security_UnsecuredAccessAllowed", "PrivateNetwork"); + return; + } + + foreach (var environmentVariable in environmentVariables) + context.EnvironmentVariables.TryAdd(environmentVariable.Key, environmentVariable.Value); + } + + /// + /// Adds a database resource to an existing RavenDB server resource. + /// + /// The resource builder for the RavenDB server. + /// The name of the database resource. + /// The name of the database to create/add. Defaults to the same name as the resource if not provided. + /// A resource builder for the newly added RavenDB database resource. + /// Thrown when the connection string cannot be retrieved during configuration. + /// Thrown when the connection string is unavailable. + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, + [ResourceName] string name, + string? databaseName = null) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + // Use the resource name as the database name if it's not provided + databaseName ??= name; + + builder.Resource.AddDatabase(name, databaseName); + var databaseResource = new RavenDBDatabaseResource(name, databaseName, builder.Resource); + + string? connectionString = null; + + builder.ApplicationBuilder.Eventing.Subscribe(databaseResource, async (@event, ct) => + { + connectionString = await databaseResource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + + if (connectionString is null) + throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{databaseResource.Name}' resource but the connection string was null."); + }); + + var healthCheckKey = $"{name}_check"; + builder.ApplicationBuilder.Services.AddHealthChecks() + .AddRavenDB(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), + databaseName: databaseName, + name: healthCheckKey); + + return builder.ApplicationBuilder + .AddResource(databaseResource); + } + + /// + /// Adds a bind mount for the data folder to a RavenDB container resource. + /// + /// The resource builder for the RavenDB server. + /// The source directory on the host to mount into the container. + /// Indicates whether the bind mount should be read-only. Defaults to false. + /// The for the RavenDB server resource. + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, "/var/lib/ravendb/data", isReadOnly); + } + + /// + /// Adds a named volume for the data folder to a RavenDB container resource. + /// + /// The resource builder for the RavenDB server. + /// Optional name for the volume. Defaults to a generated name if not provided. + /// Indicates whether the volume should be read-only. Defaults to false. + /// The for the RavenDB server resource. + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + +#pragma warning disable CTASPIRE001 + return builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/var/lib/ravendb/data", isReadOnly); +#pragma warning restore CTASPIRE001 + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBContainerImageTags.cs new file mode 100644 index 00000000..e640095f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBContainerImageTags.cs @@ -0,0 +1,7 @@ +namespace CommunityToolkit.Aspire.Hosting.RavenDB; +internal static class RavenDBContainerImageTags +{ + public const string Registry = "docker.io"; + public const string Image = "ravendb/ravendb"; + public const string Tag = "6.2-latest"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBDatabaseResource.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBDatabaseResource.cs new file mode 100644 index 00000000..fd45acec --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBDatabaseResource.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a RavenDB database. This is a child resource of a . +/// +public class RavenDBDatabaseResource(string name, string databaseName, RavenDBServerResource parent) : Resource(ThrowIfNull(name)), IResourceWithParent, IResourceWithConnectionString +{ + /// + /// Gets the parent RavenDB server resource associated with this database. + /// + public RavenDBServerResource Parent { get; } = ThrowIfNull(parent); + + /// + /// Gets the name of the database. + /// + public string DatabaseName { get; } = ThrowIfNull(databaseName); + + /// + /// Gets the connection string expression for the RavenDB database, derived from the parent server's connection string. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"{Parent};Database={DatabaseName}"); + + private static T ThrowIfNull([NotNull] T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + => argument ?? throw new ArgumentNullException(paramName); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerResource.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerResource.cs new file mode 100644 index 00000000..9514e5c5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerResource.cs @@ -0,0 +1,49 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a RavenDB container. +/// +public class RavenDBServerResource(string name, bool isSecured) : ContainerResource(name), IResourceWithConnectionString +{ + /// + /// Indicates whether the server connection is secured (HTTPS) or not (HTTP). + /// + private bool IsSecured { get; } = isSecured; + + /// + /// Gets the protocol used for the primary endpoint, based on the security setting ("http" or "https"). + /// + internal string PrimaryEndpointName => IsSecured ? "https" : "http"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the RavenDB server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + + /// + /// Gets the connection string expression for the RavenDB server, + /// formatted as "http(s)://{Host}:{Port}" depending on the security setting. + /// + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create( + $"URL={(IsSecured ? "https://" : "http://")}{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}"); + + private readonly Dictionary _databases = new(); + + /// + /// Gets a read-only dictionary of databases associated with this server resource. + /// The key represents the resource name, and the value represents the database name. + /// + public IReadOnlyDictionary Databases => _databases; + + /// + /// Adds a database to the resource. + /// + /// The name of the resource to associate with the database. + /// The name of the database to add. + internal void AddDatabase(string name, string databaseName) + { + _databases.TryAdd(name, databaseName); + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerSettings.cs b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerSettings.cs new file mode 100644 index 00000000..e722d7e9 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.RavenDB/RavenDBServerSettings.cs @@ -0,0 +1,143 @@ +namespace CommunityToolkit.Aspire.Hosting.RavenDB; + +/// +/// Represents the settings for configuring a RavenDB server resource. +/// +public class RavenDBServerSettings +{ + /// + /// The iternal URL for the RavenDB server. + /// If not specified, the container resource will automatically assign a random URL. + /// + public string? ServerUrl { get; set; } + + /// + /// The setup mode for the server. This determines whether the server is secured, uses Let's Encrypt, or is unsecured. + /// + public SetupMode SetupMode { get; private set; } + + /// + /// Gets the licensing options configured for the server. + /// + public LicensingOptions? LicensingOptions { get; private set; } + + /// + /// Protected constructor to allow inheritance but prevent direct instantiation. + /// + protected RavenDBServerSettings() { } + + /// + /// Creates an unsecured RavenDB server settings object with default settings. + /// + public static RavenDBServerSettings Unsecured() => new RavenDBServerSettings { SetupMode = SetupMode.None }; + + /// + /// Creates a secured RavenDB server settings object with the specified configuration. + /// + /// The public domain URL for the server. + /// The path to the certificate file. + /// The password for the certificate file, if required. Optional. + /// The optional server URL. + public static RavenDBServerSettings Secured(string domainUrl, string certificatePath, + string? certificatePassword = null, string? serverUrl = null) + { + return new RavenDBSecuredServerSettings(certificatePath, certificatePassword, domainUrl) + { + SetupMode = SetupMode.Secured, + ServerUrl = serverUrl + }; + } + + /// + /// Creates a secured RavenDB server settings object with the specified configuration. + /// + /// The public domain URL for the server. + /// The path to the certificate file. + /// The password for the certificate file, if required. Optional. + /// The optional server URL. + public static RavenDBServerSettings SecuredWithLetsEncrypt(string domainUrl, string certificatePath, + string? certificatePassword = null, string? serverUrl = null) + { + return new RavenDBSecuredServerSettings(certificatePath, certificatePassword, domainUrl) + { + SetupMode = SetupMode.LetsEncrypt, + ServerUrl = serverUrl + }; + } + + /// + /// Configures licensing options for the RavenDB server. + /// + /// The license string for the RavenDB server. + /// Indicates whether the End User License Agreement (EULA) has been accepted. Defaults to true. + public void WithLicense(string license, bool eulaAccepted = true) + { + LicensingOptions = new LicensingOptions(license, eulaAccepted); + } +} + +/// +/// Represents secured settings for a RavenDB server, including certificate information and a public server URL. +/// +public sealed class RavenDBSecuredServerSettings(string certificatePath, string? certificatePassword, string publicServerUrl) : RavenDBServerSettings +{ + /// + /// The path to the certificate file. + /// + public string CertificatePath { get; } = certificatePath; + + /// + /// The password for the certificate file, if required. + /// + public string? CertificatePassword { get; } = certificatePassword; + + /// + /// The public server URL (domain) that the secured RavenDB server will expose. + /// + public string PublicServerUrl { get; } = publicServerUrl; +} + +/// +/// Represents the setup modes for configuring a RavenDB server. +/// +public enum SetupMode +{ + /// + /// No specific setup mode is applied. + /// + None, + + /// + /// The server is secured using Let's Encrypt. + /// + LetsEncrypt, + + /// + /// The server is secured using a provided SSL certificate. + /// + Secured, + + /// + /// The server is unsecured. + /// + Unsecured +} + +/// +/// Represents licensing options for a RavenDB server. +/// +public sealed class LicensingOptions(string license, bool eulaAccepted = true) +{ + /// + /// RavenDB license string. + /// + public string License { get; } = license; + + /// + /// Indicates whether the End User License Agreement (EULA) has been accepted. + /// Defaults to true. + /// By setting EulaAccepted=true, you agree to the terms and conditions outlined at + /// https://ravendb.net/legal. + /// + public bool EulaAccepted { get; } = eulaAccepted; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/CommunityToolkit.Aspire.RavenDB.Client.csproj b/src/CommunityToolkit.Aspire.RavenDB.Client/CommunityToolkit.Aspire.RavenDB.Client.csproj new file mode 100644 index 00000000..458a9e9b --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/CommunityToolkit.Aspire.RavenDB.Client.csproj @@ -0,0 +1,14 @@ + + + + A .NET Aspire client integration for the RavenDB.Client library. + client ravendb + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/HealthChecksExtensions.cs b/src/CommunityToolkit.Aspire.RavenDB.Client/HealthChecksExtensions.cs new file mode 100644 index 00000000..64be4d67 --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/HealthChecksExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; + +namespace CommunityToolkit.Aspire.RavenDB.Client; + +internal static class HealthChecksExtensions +{ + /// + /// Adds a HealthCheckRegistration if one hasn't already been added to the builder. + /// + public static void TryAddHealthCheck(this IHostApplicationBuilder builder, HealthCheckRegistration healthCheckRegistration) + { + builder.TryAddHealthCheck(healthCheckRegistration.Name, hcBuilder => hcBuilder.Add(healthCheckRegistration)); + } + + /// + /// Invokes the action if the given hasn't already been added to the builder. + /// + public static void TryAddHealthCheck(this IHostApplicationBuilder builder, string name, Action addHealthCheck) + { + var healthCheckKey = $"Aspire.HealthChecks.{name}"; + if (!builder.Properties.ContainsKey(healthCheckKey)) + { + builder.Properties[healthCheckKey] = true; + addHealthCheck(builder.Services.AddHealthChecks()); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..ae519889 --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/PublicAPI.Unshipped.txt @@ -0,0 +1,28 @@ +#nullable enable +Microsoft.Extensions.Hosting.RavenDBClientExtension +static Microsoft.Extensions.Hosting.RavenDBClientExtension.AddKeyedRavenDBClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, object! serviceKey, CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings! settings) -> void +static Microsoft.Extensions.Hosting.RavenDBClientExtension.AddKeyedRavenDBClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, object! serviceKey, string! connectionName, System.Action? configureSettings = null) -> void +static Microsoft.Extensions.Hosting.RavenDBClientExtension.AddRavenDBClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings! settings) -> void +static Microsoft.Extensions.Hosting.RavenDBClientExtension.AddRavenDBClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action? configureSettings = null) -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.RavenDBClientSettings() -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.Urls.get -> string![]? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.Urls.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CertificatePath.get -> string? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CertificatePath.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CertificatePassword.get -> string? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CertificatePassword.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.Certificate.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DatabaseName.get -> string? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DatabaseName.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CreateDatabase.get -> bool +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.CreateDatabase.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.ModifyDocumentStore.get -> System.Action? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.ModifyDocumentStore.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DisableHealthChecks.get -> bool +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DisableHealthChecks.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DisableTracing.get -> bool +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.DisableTracing.set -> void +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.HealthCheckTimeout.get -> int? +CommunityToolkit.Aspire.RavenDB.Client.RavenDBClientSettings.HealthCheckTimeout.set -> void diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/README.md b/src/CommunityToolkit.Aspire.RavenDB.Client/README.md new file mode 100644 index 00000000..aa858538 --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/README.md @@ -0,0 +1,141 @@ +# CommunityToolkit.Aspire.RavenDB.Client library + +Registers `IDocumentStore` and the associated `IDocumentSession` and `IAsyncDocumentSession` instances in the DI container for connecting to a RavenDB database. Additionally, it enables health checks, metrics, logging, and telemetry. + +## Getting started + +### Prerequisites + +- RavenDB database and connection string for accessing the database or a running RavenDB server instance with its connection details, such as the server's URL and a valid certificate if required. + +_**Note:** +RavenDB allows creating an `IDocumentStore` without a defined database. In such cases, `IDocumentSession` and `IAsyncDocumentSession` will not be available. This library also supports creating a new RavenDB database. However, if you intend to connect to an existing RavenDB database, ensure the database exists and you have its connection details._ + +### Install the package + +Install the `CommunityToolkit.Aspire.RavenDB.Client` library with [NuGet](https://www.nuget.org): +```dotnetcli +dotnet add package CommunityToolkit.Aspire.RavenDB.Client +``` +*To be added once the package is published.* + +## Usage example + +In the _Program.cs_ file of your project, call the `AddRavenDBClient` extension method to register a `IDocumentStore` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddRavenDBClient("ravendb"); +``` + +You can then retrieve a `IDocumentStore` instance using dependency injection, for example: + +```csharp +public class MyService +{ + private readonly IDocumentStore _documentStore; + public MyService(IDocumentStore documentStore) + { + _documentStore = documentStore; + } + + // Your logic here +} +``` + +## Configuration + +The .NET Aspire RavenDB Client component provides multiple options to configure the server connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddRavenDBClient()`: + +```csharp +builder.AddRavenDBClient("ravendb"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "ravendb": "URL=http://localhost:8080;Database=ravenDatabase" + } +} +``` + +### Use configuration providers + +The .NET Aspire RavenDB Client component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `RavenDBClientSettings` from configuration by using the `Aspire:RavenDB:Client` key. + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the database name and certificate from code: + +```csharp +builder.AddRavenDBClient("ravendb", settings => +{ + settings.DatabaseName = "ravenDatabase"; + settings.Certificate = ravenCertificate; +}); +``` + +### Use RavenDBClientSettings Class + +The RavenDBClientSettings class simplifies configuration by allowing you to specify: +- URLs of your RavenDB nodes. +- Database name to connect to or create. +- Certificate details (via `CertificatePath` and `CertificatePassword` or `Certificate`). +- Optional actions to modify the `IDocumentStore`. + +Example for creating a new database on a local unsecured RavenDB server: + +```csharp +var settings = new RavenDBClientSettings(new[] { “http://127.0.0.1:8080” }, “NorthWind”) +{ + CreateDatabase = true; +}; +builder.AddRavenDBClient(settings); +``` + +You can also configure: +- `DisableHealthChecks` to disable health checks. +- `HealthCheckTimeout` to set the timeout for health checks. +- `DisableTracing` to disable `OpenTelemetry` tracing. + +## AppHost extensions + +### Install the CommunityToolkit.Aspire.Hosting.RavenDB Library + +Install the `CommunityToolkit.Aspire.Hosting.RavenDB` library with [NuGet](https://www.nuget.org): +```dotnetcli +dotnet add package CommunityToolkit.Aspire.Hosting.RavenDB +``` +*To be added once the package is published.* + +### Usage in AppHost + +In your AppHost's _Program.cs_ file, register a RavenDB server resource and consume the connection using the following methods: + +```csharp +var elasticsearch = builder.AddRavenDB("ravendb"); + +var myService = builder.AddProject() + .WithReference(ravendb); +``` + +The `WithReference` method configures a connection in the `MyService` project named `ravendb`. In the _Program.cs_ file of `MyService`, the RavenDB connection can be consumed using: + +```csharp +builder.AddRavenDBClient("ravendb"); +``` + +## Additional Documentation + +- https://ravendb.net/docs/article-page/6.2/csharp +- https://github.com/ravendb/ravendb +- https://learn.microsoft.com/dotnet/aspire/community-toolkit/ravendb + +## Feedback & Contributing + +https://github.com/CommunityToolkit/Aspire \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientExtension.cs b/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientExtension.cs new file mode 100644 index 00000000..c9e2e61a --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientExtension.cs @@ -0,0 +1,302 @@ +using CommunityToolkit.Aspire.RavenDB.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using Raven.Client.ServerWide; +using Raven.Client.ServerWide.Operations; +using System.Data.Common; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for connecting RavenDB database. +/// +public static class RavenDBClientExtension +{ + private const string ActivityNameSource = "RavenDB.Client.DiagnosticSources"; + + private const string DefaultConfigSectionName = "Aspire:RavenDB:Client"; + + /// + /// Registers and the associated and + /// instances for connecting to an existing or new RavenDB database with RavenDB.Client. + /// + /// The used to add services. + /// The name used to retrieve the connection string from the "ConnectionStrings" configuration section. + /// An optional delegate that can be used for customizing options. It is invoked after the settings are read from the configuration. + /// Notes: + /// + /// Reads the configuration from "Aspire:RavenDB:Client" section. + /// The is registered as a singleton, meaning a single instance is shared throughout the application's lifetime, + /// while and are registered per request to ensure short-lived session instances for each use. + /// + /// + public static void AddRavenDBClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null) + { + var settings = GetRavenDBClientSettings(builder, connectionName, configureSettings); + + builder.AddRavenDBClientInternal(settings); + } + + /// + /// Registers and the associated and + /// instances for connecting to an existing or new RavenDB database with RavenDB.Client, identified by a unique service key. + /// + /// The used to add services. + /// A unique key that identifies this instance of the RavenDB client service. + /// The name used to retrieve the connection string from the "ConnectionStrings" configuration section. + /// An optional delegate that can be used for customizing options. It is invoked after the settings are read from the configuration. + /// Notes: + /// + /// Reads the configuration from "Aspire:RavenDB:Client" section. + /// The is registered as a singleton, meaning a single instance is shared throughout the application's lifetime, + /// while and are registered per request to ensure short-lived session instances for each use. + /// + /// + public static void AddKeyedRavenDBClient( + this IHostApplicationBuilder builder, + object serviceKey, + string connectionName, + Action? configureSettings = null) + { + var settings = GetRavenDBClientSettings(builder, connectionName, configureSettings); + + builder.AddRavenDBClientInternal(settings, serviceKey); + } + + /// + /// Registers and the associated and + /// instances for connecting to an existing or new RavenDB database with RavenDB.Client. + /// + /// The used to add services. + /// The settings required to configure the . + /// Notes: + /// + /// If is not specified and is set to 'false', + /// and will not be registered. + /// The is registered as a singleton, meaning a single instance is shared throughout the application's lifetime, + /// while and are registered per request to ensure short-lived session instances for each use. + /// + /// + public static void AddRavenDBClient( + this IHostApplicationBuilder builder, + RavenDBClientSettings settings) + { + builder.AddRavenDBClientInternal(settings); + } + + /// + /// Registers and the associated and + /// instances for connecting to an existing or new RavenDB database with RavenDB.Client, identified by a unique service key. + /// + /// The used to add services. + /// A unique key that identifies this instance of the RavenDB client service. + /// The settings required to configure the . + /// Notes: + /// + /// If is not specified and is set to 'false', + /// and will not be registered. + /// The is registered as a singleton, meaning a single instance is shared throughout the application's lifetime, + /// while and are registered per request to ensure short-lived session instances for each use. + /// + /// + public static void AddKeyedRavenDBClient( + this IHostApplicationBuilder builder, + object serviceKey, + RavenDBClientSettings settings) + { + builder.AddRavenDBClientInternal(settings, serviceKey); + } + + private static void AddRavenDBClientInternal( + this IHostApplicationBuilder builder, + RavenDBClientSettings settings, + object? serviceKey = null) + { + ValidateSettings(builder, settings); + + var documentStore = CreateRavenClient(settings); + + if (serviceKey is null) + { + builder + .Services + .AddSingleton(documentStore); + } + else + { + builder + .Services + .AddKeyedSingleton(serviceKey, documentStore); + } + + builder.AddRavenDocumentSession(documentStore, serviceKey); + + if (!settings.DisableTracing) + { + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddSource(ActivityNameSource); + }); + } + + builder.AddHealthCheck( + serviceKey is null ? "RavenDB.Client" : $"RavenDB.Client_{serviceKey}", + settings); + } + + private static void AddRavenDocumentSession( + this IHostApplicationBuilder builder, + IDocumentStore documentStore, + object? serviceKey) + { + if (string.IsNullOrWhiteSpace(documentStore.Database)) + return; + + // AddTransient creates new instance per request/usage which is ideal for document sessions + + if (serviceKey is null) + { + builder.Services.AddTransient(provider => + provider.CreateDocumentSession(documentStore)); + + builder.Services.AddTransient(provider => + provider.CreateAsyncDocumentSession(documentStore)); + + return; + } + + builder.Services.AddKeyedTransient(serviceKey, + (sp, _) => sp.CreateDocumentSession(documentStore)); + + builder.Services.AddKeyedTransient(serviceKey, + (sp, _) => sp.CreateAsyncDocumentSession(documentStore)); + } + + private static void AddHealthCheck( + this IHostApplicationBuilder builder, + string healthCheckName, + RavenDBClientSettings settings) + { + if (settings.DisableHealthChecks) + return; + + builder.TryAddHealthCheck( + healthCheckName, + healthCheck => healthCheck.AddRavenDB(options => + { + options.Database = settings.DatabaseName; + options.Urls = settings.Urls!; + options.Certificate = settings.GetCertificate(); + }, + healthCheckName, + null, + null, + settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null)); + } + + private static IDocumentStore CreateRavenClient(RavenDBClientSettings ravenDbClientSettings) + { + var documentStore = new DocumentStore() + { + Urls = ravenDbClientSettings.Urls, + Database = ravenDbClientSettings.DatabaseName, + Certificate = ravenDbClientSettings.GetCertificate() + }; + + ravenDbClientSettings.ModifyDocumentStore?.Invoke(documentStore); + + documentStore.Initialize(); + + if (ravenDbClientSettings.CreateDatabase) + { + var databaseRecord = documentStore.Maintenance.Server.Send(new GetDatabaseRecordOperation(ravenDbClientSettings.DatabaseName)); + if (databaseRecord == null) + { + var newDatabaseRecord = new DatabaseRecord(ravenDbClientSettings.DatabaseName); + documentStore.Maintenance.Server.Send(new CreateDatabaseOperation(newDatabaseRecord)); + } + } + + return documentStore; + } + + private static IDocumentSession CreateDocumentSession(this IServiceProvider provider, + IDocumentStore documentStore) => documentStore.OpenSession(); + + private static IAsyncDocumentSession CreateAsyncDocumentSession(this IServiceProvider provider, + IDocumentStore documentStore) => documentStore.OpenAsyncSession(); + + private static RavenDBClientSettings GetRavenDBClientSettings(this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings) + { + var configSection = builder.Configuration.GetSection(DefaultConfigSectionName); + var namedConfigSection = configSection.GetSection(connectionName); + + var settings = new RavenDBClientSettings(); + configSection.Bind(settings); + namedConfigSection.Bind(settings); + + var connectionString = builder.Configuration.GetConnectionString(connectionName); + if (string.IsNullOrEmpty(connectionString) == false) + { + var connectionBuilder = new DbConnectionStringBuilder + { + ConnectionString = connectionString + }; + + if (connectionBuilder.TryGetValue("URL", out var url) && url is string serverUrl) + { + settings.Urls = new[] { serverUrl }; + } + + if (connectionBuilder.TryGetValue("Database", out var database) && database is string databaseName) + { + settings.DatabaseName = databaseName; + } + } + + configureSettings?.Invoke(settings); + + return settings; + } + + private static void ValidateSettings( + IHostApplicationBuilder builder, + RavenDBClientSettings settings) + { + ArgumentNullException.ThrowIfNull(builder); + + if (settings.Urls is null || settings.Urls.Length == 0) + throw new ArgumentNullException(nameof(settings.Urls), "At least one connection URL must be provided."); + + if (settings.CreateDatabase && string.IsNullOrWhiteSpace(settings.DatabaseName)) + throw new ArgumentNullException(nameof(settings.DatabaseName), "A database name must be specified in 'RavenDBClientSettings.DatabaseName' " + + "when 'RavenDBClientSettings.CreateDatabase' is set to true."); + + foreach (var url in settings.Urls) + { + if (IsValidUrl(url, out var uri) == false) + throw new ArgumentException($"The provided URL '{url}' is invalid. Please provide a valid HTTP or HTTPS URL."); + + if (uri!.Scheme == Uri.UriSchemeHttps) + { + if (string.IsNullOrEmpty(settings.CertificatePath) && settings.Certificate == null) + throw new ArgumentNullException(nameof(settings.Certificate), "A valid certificate must be provided in 'RavenDBClientSettings.Certificate' " + + "or a certificate path in 'RavenDBClientSettings.CertificatePath' when using HTTPS."); + } + } + + bool IsValidUrl(string url, out Uri? uriResult) + { + return Uri.TryCreate(url, UriKind.Absolute, out uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } + } +} diff --git a/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientSettings.cs b/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientSettings.cs new file mode 100644 index 00000000..a923c990 --- /dev/null +++ b/src/CommunityToolkit.Aspire.RavenDB.Client/RavenDBClientSettings.cs @@ -0,0 +1,84 @@ +using Raven.Client.Documents; +using System.Security.Cryptography.X509Certificates; + +namespace CommunityToolkit.Aspire.RavenDB.Client; + +/// +/// Provides the client configuration settings for connecting to a RavenDB database. +/// +public sealed class RavenDBClientSettings +{ + /// + /// The URLs of the RavenDB server nodes. + /// + public string[]? Urls { get; set; } + + /// + /// The path to the certificate file. + /// + public string? CertificatePath { get; set; } + + /// + /// The password for the certificate. + /// + public string? CertificatePassword { get; set; } + + /// + /// The certificate for RavenDB server. + /// + public X509Certificate2? Certificate { get; set; } + + /// + /// The name of the database to connect to. + /// + public string? DatabaseName { get; set; } + + /// + /// Gets or sets a value indicating whether a new database should be created if it does not already exist. + /// If set to and a database with the specified name already exists, the existing database will be used. + /// The default value is . + /// + public bool CreateDatabase { get; set; } + + /// + /// Action that allows modifications of the . + /// + public Action? ModifyDocumentStore { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether RavenDB health check is disabled or not. + /// The default value is . + /// + public bool DisableHealthChecks { get; set; } + + /// + /// Gets or sets the timeout in milliseconds for the RavenDB health check. + /// + public int? HealthCheckTimeout { get; set; } + + /// + /// Gets or sets a value indicating whether OpenTelemetry tracing is disabled. + /// The default value is . + /// + public bool DisableTracing { get; set; } + + /// + /// Retrieves the used for authentication, if a certificate path is specified. + /// + /// An instance if the is specified; + /// otherwise, . + internal X509Certificate2? GetCertificate() + { + if (Certificate != null) + return Certificate; + + if (string.IsNullOrEmpty(CertificatePath)) + { + return null; + } + +#pragma warning disable SYSLIB0057 + return new X509Certificate2(CertificatePath, CertificatePassword); +#pragma warning restore SYSLIB0057 + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AddRavenDBTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AddRavenDBTests.cs new file mode 100644 index 00000000..6dda5317 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AddRavenDBTests.cs @@ -0,0 +1,136 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.RavenDB.Tests; + +public class AddRavenDBTests +{ + [Fact] + public void AddRavenServerResource() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddRavenDB("ravenServer"); + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var serverResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("ravenServer", serverResource.Name); + } + + [Fact] + public void AddRavenServerAndDatabaseResource() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddRavenDB("ravenServer").AddDatabase(name: "ravenDatabase", databaseName: "TestDatabase"); + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var serverResource = Assert.Single(appModel.Resources.OfType()); + var databaseResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("ravenServer", serverResource.Name); + Assert.Equal("ravenDatabase", databaseResource.Name); + Assert.Equal("TestDatabase", databaseResource.DatabaseName); + Assert.True(serverResource.Databases.TryGetValue("ravenDatabase", out var databaseName)); + Assert.Equal("TestDatabase", databaseName); + } + + [Fact] + public void VerifyNonDefaultImageTag() + { + var tag = "windows-latest-lts"; + + var builder = DistributedApplication.CreateBuilder(); + builder.AddRavenDB("raven").WithImageTag(tag); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + var annotation = Assert.Single(annotations); + Assert.NotNull(annotation.Tag); + Assert.Equal(tag, annotation.Tag); + } + + [Fact] + public void VerifyDefaultPort() + { + var builder = DistributedApplication.CreateBuilder(); + builder.AddRavenDB("raven"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoint = Assert.Single(resource.Annotations.OfType()); + + Assert.Equal(8080, endpoint.TargetPort); + } + + [Fact] + public void SpecifiedDataVolumeNameIsUsed() + { + var builder = DistributedApplication.CreateBuilder(); + _ = builder.AddRavenDB("raven").WithDataVolume("data"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + + var annotation = Assert.Single(annotations); + + Assert.Equal("data", annotation.Source); + } + + [Theory] + [InlineData("data")] + [InlineData(null)] + public void CorrectTargetPathOnVolumeMount(string? volumeName) + { + var builder = DistributedApplication.CreateBuilder(); + _ = builder.AddRavenDB("raven").WithDataVolume(volumeName); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + + var annotation = Assert.Single(annotations); + + Assert.Equal("/var/lib/ravendb/data", annotation.Target); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadOnlyVolumeMount(bool isReadOnly) + { + var builder = DistributedApplication.CreateBuilder(); + _ = builder.AddRavenDB("raven").WithDataVolume(isReadOnly: isReadOnly); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + + var annotation = Assert.Single(annotations); + + Assert.Equal(isReadOnly, annotation.IsReadOnly); + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AppHostTests.cs new file mode 100644 index 00000000..7279cac9 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/AppHostTests.cs @@ -0,0 +1,70 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Raven.Client.Documents; + +namespace CommunityToolkit.Aspire.Hosting.RavenDB.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task TestAppHost() + { + using var cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(TimeSpan.FromMinutes(5)); + + var connectionName = "ravendb"; + var databaseName = "ravenDatabase"; + + await fixture.ResourceNotificationService.WaitForResourceAsync(connectionName, KnownResourceStates.Running, cancellationToken.Token).WaitAsync(TimeSpan.FromMinutes(5), cancellationToken.Token); + + var endpoint = fixture.GetEndpoint(connectionName, "http"); + Assert.NotNull(endpoint); + Assert.False(string.IsNullOrWhiteSpace(endpoint.OriginalString)); + Assert.True(endpoint.Scheme == Uri.UriSchemeHttp); + + var appModel = fixture.App.Services.GetRequiredService(); + var serverResource = Assert.Single(appModel.Resources.OfType()); + var dbResource = Assert.Single(appModel.Resources.OfType()); + + var serverConnectionString = await serverResource.ConnectionStringExpression.GetValueAsync(cancellationToken.Token); + Assert.False(string.IsNullOrWhiteSpace(serverConnectionString)); + Assert.Contains(endpoint.OriginalString, serverConnectionString); + Assert.Equal(databaseName, dbResource.DatabaseName); + + var databaseConnectionString = await dbResource.ConnectionStringExpression.GetValueAsync(cancellationToken.Token); + Assert.False(string.IsNullOrWhiteSpace(databaseConnectionString)); + Assert.Equal($"URL={endpoint.OriginalString};Database={databaseName}", databaseConnectionString); + Assert.Equal(databaseName, dbResource.DatabaseName); + + // Create RavenDB Client + + var clientBuilder = Host.CreateApplicationBuilder(); + clientBuilder.Configuration.AddInMemoryCollection([ + new KeyValuePair($"ConnectionStrings:{connectionName}", databaseConnectionString) + ]); + + clientBuilder.AddRavenDBClient(connectionName: connectionName, configureSettings: settings => + { + settings.CreateDatabase = true; + }); + var host = clientBuilder.Build(); + + using var documentStore = host.Services.GetRequiredService(); + + using (var session = documentStore.OpenAsyncSession()) + { + await session.StoreAsync(new { Id = "Test/1", Name = "Test Document" }, cancellationToken.Token); + await session.SaveChangesAsync(cancellationToken.Token); + } + + using (var session = documentStore.OpenAsyncSession()) + { + var doc = await session.LoadAsync("Test/1", cancellationToken.Token); + Assert.NotNull(doc); + Assert.Equal("Test Document", doc.Name.ToString()); + } + } +} diff --git a/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests.csproj new file mode 100644 index 00000000..c13e6aa6 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests/CommunityToolkit.Aspire.Hosting.RavenDB.Tests.csproj @@ -0,0 +1,15 @@ + + + + enable + enable + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/AspireRavenDBExtensionsTests.cs b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/AspireRavenDBExtensionsTests.cs new file mode 100644 index 00000000..dedebb75 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/AspireRavenDBExtensionsTests.cs @@ -0,0 +1,377 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using Raven.Client.Http; + +namespace CommunityToolkit.Aspire.RavenDB.Client.Tests; + +public class AspireRavenDBExtensionsTests(RavenDbServerFixture serverFixture) : IClassFixture +{ + private const string DefaultConnectionName = "ravendb"; + private string DefaultConnectionString => serverFixture.ConnectionString ?? + throw new InvalidOperationException("The server was not initialized."); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AddKeyedRavenDbClient_ReadsFromConnectionStringsCorrectly(bool shouldCreateDatabase) + { + var builder = CreateBuilder(); + + string? databaseName = null; + Action? configSettings = null; + if (shouldCreateDatabase) + { + databaseName = Guid.NewGuid().ToString("N"); + configSettings = clientSettings => + { + clientSettings.DatabaseName = databaseName; + clientSettings.CreateDatabase = true; + }; + } + + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: configSettings); + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredKeyedService(DefaultConnectionName); + Assert.Equal(DefaultConnectionString, documentStore.Urls[0]); + + if (shouldCreateDatabase) + { + Assert.Equal(databaseName, documentStore.Database); + + using var session = host.Services.GetRequiredKeyedService(DefaultConnectionName); + Assert.NotNull(session); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AddRavenDbClientWithSettingsShouldWork(bool shouldCreateDatabase) + { + var builder = CreateBuilder(); + + string? databaseName = null; + if (shouldCreateDatabase) + databaseName = Guid.NewGuid().ToString("N"); + + var settings = GetDefaultSettings(databaseName, shouldCreateDatabase); + + builder.AddRavenDBClient(settings: settings); + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredService(); + Assert.Equal(DefaultConnectionString, documentStore.Urls[0]); + + if (shouldCreateDatabase) + { + Assert.Equal(databaseName, documentStore.Database); + + using var session = host.Services.GetRequiredService(); + Assert.NotNull(session); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AddKeyedRavenDbClientWithSettingsShouldWork(bool shouldCreateDatabase) + { + var builder = CreateBuilder(); + + string? databaseName = null; + if (shouldCreateDatabase) + databaseName = Guid.NewGuid().ToString("N"); + + var settings = GetDefaultSettings(databaseName, shouldCreateDatabase); + + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, settings: settings); + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredKeyedService(DefaultConnectionName); + Assert.Equal(DefaultConnectionString, documentStore.Urls[0]); + + if (shouldCreateDatabase) + { + Assert.Equal(databaseName, documentStore.Database); + + using var session = host.Services.GetRequiredKeyedService(DefaultConnectionName); + using var asyncSession = host.Services.GetRequiredKeyedService(DefaultConnectionName); + Assert.NotNull(session); + Assert.NotNull(asyncSession); + } + } + + [Fact] + public void AddKeyedRavenDbClientAndOpen2SessionShouldNotBeEqual() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.DatabaseName = databaseName; + clientSettings.CreateDatabase = true; + }); + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredKeyedService(DefaultConnectionName); + + using var session1 = host.Services.GetRequiredKeyedService(DefaultConnectionName); + using var session2 = host.Services.GetRequiredKeyedService(DefaultConnectionName); + + Assert.NotEqual(session1, session2); + } + + [Fact] + public void AddRavenDbClientAndOpen2SessionShouldNotBeEqual() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + builder.AddRavenDBClient(connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.DatabaseName = databaseName; + clientSettings.CreateDatabase = true; + }); + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredService(); + + using var session1 = host.Services.GetRequiredService(); + using var session2 = host.Services.GetRequiredService(); + + Assert.NotEqual(session1, session2); + } + + [Fact] + public void AddKeyedRavenDbClientThenGetRequiredServiceShouldThrow() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: settings => settings.DatabaseName = databaseName); + using var host = builder.Build(); + + Assert.Throws(() => host.Services.GetRequiredService()); + } + + [Fact] + public void AddKeyedRavenDbClientShouldWork_Create2NewDatabases() + { + var databaseName1 = Guid.NewGuid().ToString("N"); + var databaseName2 = Guid.NewGuid().ToString("N"); + + var connectionName1 = Guid.NewGuid().ToString("N"); + var connectionName2 = Guid.NewGuid().ToString("N"); + + IEnumerable> config = + [ + new KeyValuePair($"ConnectionStrings:{connectionName1}", $"URL={DefaultConnectionString}"), + new KeyValuePair($"ConnectionStrings:{connectionName2}", $"URL={DefaultConnectionString}") + ]; + var builder = CreateBuilder(config); + + builder.AddKeyedRavenDBClient(serviceKey: connectionName1, connectionName: connectionName1, configureSettings: clientSettings => + { + clientSettings.DatabaseName = databaseName1; + clientSettings.CreateDatabase = true; + }); + builder.AddKeyedRavenDBClient(serviceKey: connectionName2, connectionName: connectionName2, configureSettings: clientSettings => + { + clientSettings.DatabaseName = databaseName2; + clientSettings.CreateDatabase = true; + }); + + using var host = builder.Build(); + + using var documentStore1 = host.Services.GetRequiredKeyedService(connectionName1); + using var documentStore2 = host.Services.GetRequiredKeyedService(connectionName2); + + Assert.NotNull(documentStore1); + Assert.NotNull(documentStore2); + + using var session1 = host.Services.GetRequiredKeyedService(connectionName1); + using var session2 = host.Services.GetRequiredKeyedService(connectionName2); + + Assert.Equal(DefaultConnectionString, documentStore1.Urls[0]); + Assert.Equal(DefaultConnectionString, documentStore2.Urls[0]); + + Assert.Equal(databaseName1, documentStore1.Database); + Assert.Equal(databaseName2, documentStore2.Database); + + Assert.NotEqual(session1, session2); + } + + [Fact] + public async Task AddRavenDbClient_HealthCheckShouldBeRegisteredWhenEnabled() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddRavenDBClient(connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + }); + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetRequiredService(); + + var healthCheckReport = await healthCheckService.CheckHealthAsync(); + + var healthCheckName = "RavenDB.Client"; + + Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); + } + + [Fact] + public void AddRavenDbClient_HealthCheckShouldNotBeRegisteredWhenDisabled() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddRavenDBClient(connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + clientSettings.DisableHealthChecks = true; + }); + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetService(); + + Assert.Null(healthCheckService); + } + + [Fact] + public async Task AddKeyedRavenDbClient_HealthCheckShouldBeRegisteredWhenEnabled() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + }); + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetRequiredService(); + + var healthCheckReport = await healthCheckService.CheckHealthAsync(); + + var healthCheckName = $"RavenDB.Client_{DefaultConnectionName}"; + + Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName); + } + + [Fact] + public void AddKeyedRavenDbClient_HealthCheckShouldNotBeRegisteredWhenDisabled() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + clientSettings.DisableHealthChecks = true; + }); + + using var host = builder.Build(); + + var healthCheckService = host.Services.GetService(); + + Assert.Null(healthCheckService); + } + + [Fact] + public void AddRavenDbClient_ModifyDocumentStore() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddRavenDBClient(connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + clientSettings.ModifyDocumentStore = store => + { + store.Conventions.ReadBalanceBehavior = ReadBalanceBehavior.RoundRobin; + store.Conventions.MaxNumberOfRequestsPerSession = 99; + }; + }); + + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredService(); + + Assert.NotNull(documentStore); + Assert.Equal(databaseName, documentStore.Database); + Assert.Equal(ReadBalanceBehavior.RoundRobin, documentStore.Conventions.ReadBalanceBehavior); + Assert.Equal(99, documentStore.Conventions.MaxNumberOfRequestsPerSession); + } + + [Fact] + public void AddKeyedRavenDbClient_ModifyDocumentStore() + { + var databaseName = Guid.NewGuid().ToString("N"); + + var builder = CreateBuilder(); + + builder.AddKeyedRavenDBClient(serviceKey: DefaultConnectionName, connectionName: DefaultConnectionName, configureSettings: clientSettings => + { + clientSettings.CreateDatabase = true; + clientSettings.DatabaseName = databaseName; + clientSettings.ModifyDocumentStore = store => + { + store.Conventions.ReadBalanceBehavior = ReadBalanceBehavior.RoundRobin; + store.Conventions.MaxNumberOfRequestsPerSession = 99; + }; + }); + + using var host = builder.Build(); + + using var documentStore = host.Services.GetRequiredKeyedService(DefaultConnectionName); + + Assert.NotNull(documentStore); + Assert.Equal(databaseName, documentStore.Database); + Assert.Equal(ReadBalanceBehavior.RoundRobin, documentStore.Conventions.ReadBalanceBehavior); + Assert.Equal(99, documentStore.Conventions.MaxNumberOfRequestsPerSession); + } + + private HostApplicationBuilder CreateBuilder(IEnumerable>? config = null) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + + builder.Configuration.AddInMemoryCollection(config ?? GetDefaultConfiguration()); + return builder; + } + + private IEnumerable> GetDefaultConfiguration() => + [ + new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", $"URL={DefaultConnectionString}") + ]; + + private RavenDBClientSettings GetDefaultSettings(string? databaseName = null, bool shouldCreateDatabase = true) + { + return new RavenDBClientSettings + { + Urls = new[] { DefaultConnectionString }, + DatabaseName = databaseName, + CreateDatabase = shouldCreateDatabase + }; + } +} diff --git a/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/CommunityToolkit.Aspire.RavenDB.Client.Tests.csproj b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/CommunityToolkit.Aspire.RavenDB.Client.Tests.csproj new file mode 100644 index 00000000..b4528f84 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/CommunityToolkit.Aspire.RavenDB.Client.Tests.csproj @@ -0,0 +1,18 @@ + + + + Exe + enable + enable + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/RavenDbServerFixture.cs b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/RavenDbServerFixture.cs new file mode 100644 index 00000000..2df45c92 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.RavenDB.Client.Tests/RavenDbServerFixture.cs @@ -0,0 +1,42 @@ +using Raven.Client.Documents; +using Raven.Embedded; + +namespace CommunityToolkit.Aspire.RavenDB.Client.Tests; + +public sealed class RavenDbServerFixture : IAsyncLifetime, IDisposable +{ + public EmbeddedServer Server { get; } = EmbeddedServer.Instance; + public IDocumentStore? Store { get; private set; } + public string? ConnectionString { get; private set; } + + public async Task InitializeAsync() + { + Server.StartServer(); + + var uri = await Server.GetServerUriAsync(); + var serverUrl = uri.OriginalString; + ConnectionString = serverUrl; + + Store = new DocumentStore { Urls = new[] { serverUrl } }; + } + + public void CreateDatabase(string databaseName) => + CreateDatabase(new DatabaseOptions(databaseName)); + + public void CreateDatabase(DatabaseOptions options) + { + Store = Server.GetDocumentStore(options); + } + + public Task DisposeAsync() + { + Dispose(); + return Task.CompletedTask; + } + + public void Dispose() + { + Store?.Dispose(); + Server.Dispose(); + } +}