From fb9e2a62f6f79717b34d4da250c29e74d082bba1 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:14:44 +0100 Subject: [PATCH 1/2] feat(floci): add WithCosmosReference for the Floci Azure emulator Adds a WithCosmosReference(...) extension (TS binding withFlociAzureCosmosReference) that injects a Cosmos DB connection string (ConnectionStrings__{connectionName}, default 'cosmos') pointing at the floci-az emulator's Cosmos SQL/NoSQL API, so a dependent can consume it via the standard AddAzureCosmosClient(name) flow. connectionName and accountName (default devstoreaccount1) are configurable; uses the well-known Cosmos emulator key. Additive/composable with WithReference. Includes tests, docs, and the regenerated API surface. Closes CommunityToolkit/Aspire#1545 --- .../FlociAzureContainerResource.cs | 4 ++ .../FlociHostingExtension.Azure.cs | 49 +++++++++++++ .../README.md | 33 +++++++++ .../CommunityToolkit.Aspire.Hosting.Floci.cs | 4 ++ .../WithReferenceTests.cs | 72 +++++++++++++++++++ 5 files changed, 162 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs index 9286669a2..5bb84f7d1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs @@ -14,6 +14,10 @@ public class FlociAzureContainerResource(string name) : FlociContainerResource(n internal const string DefaultAccountName = "devstoreaccount1"; internal const string DefaultAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMh0=="; + // Well-known Cosmos DB emulator account key that floci-az accepts by default (no auth enforced). + internal const string DefaultCosmosAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + internal const string DefaultCosmosConnectionName = "cosmos"; + internal override void ApplyUIEnvironment(EnvironmentCallbackContext context) { context.EnvironmentVariables[FlociUIContainerResource.AzureEndpointEnvVar] = UIEndpointExpression; diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs index bd6100db2..784493844 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs @@ -73,6 +73,55 @@ public static IResourceBuilder WithReference( }); + /// + /// Adds a Cosmos DB connection string for the Floci Azure emulator to a dependent resource, + /// injecting ConnectionStrings__{connectionName} so a Cosmos client — e.g. + /// AddAzureCosmosClient(connectionName) — resolves to the emulator. floci-az serves the + /// Cosmos SQL/NoSQL API from the same endpoint under the {account}-cosmos path. + /// + /// + /// This is additive and composes with : + /// it injects only the Cosmos connection string, not the base ConnectionStrings__{name} + /// reference, so call both if you also want the base endpoint / storage variables. The endpoint + /// is left as an unresolved expression, so Aspire resolves it per dependent (a project gets + /// localhost:{hostPort}, a sibling container the container-network address) and a + /// certificate configured later still flips the scheme to https. + /// + /// Adds a Cosmos DB connection string for the Floci Azure emulator + /// The type of the resource receiving the reference. + /// The resource builder for the resource receiving the reference. + /// The Floci Azure resource to reference. + /// The connection string name to inject (default: cosmos). + /// The Cosmos account name segment (default: devstoreaccount1). + /// A reference to the for further configuration. + [AspireExport("withFlociAzureCosmosReference")] + public static IResourceBuilder WithCosmosReference( + this IResourceBuilder builder, + IResourceBuilder floci, + string connectionName = FlociAzureContainerResource.DefaultCosmosConnectionName, + string? accountName = null) + where TDestination : IResourceWithEnvironment + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(floci); + ArgumentException.ThrowIfNullOrEmpty(connectionName); + + FlociAzureContainerResource resource = floci.Resource; + string account = accountName ?? FlociAzureContainerResource.DefaultAccountName; + + // Built inside the callback so the scheme is read after the whole AppHost is configured. + // The account key is the well-known Cosmos emulator key, distinct from the Azurite storage + // key used for AZURE_STORAGE_CONNECTION_STRING. + return builder.WithEnvironment(context => + { + ReferenceExpression accountEndpoint = ReferenceExpression.Create( + $"{resource.ConnectionStringExpression}/{account}-cosmos/"); + + context.EnvironmentVariables[$"ConnectionStrings__{connectionName}"] = ReferenceExpression.Create( + $"AccountEndpoint={accountEndpoint};AccountKey={FlociAzureContainerResource.DefaultCosmosAccountKey};"); + }); + } + /// /// Mounts the Docker socket into the Floci Azure container so that Azure Functions and other /// container-backed services can launch sibling containers. diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md index 7cbd9b811..0cb4762c1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md @@ -73,6 +73,39 @@ await builder.addProject('api', '../MyApi/MyApi.csproj') | `ConnectionStrings__floci-az` | `http://localhost:{port}` (standard Aspire connection string) | | `AZURE_STORAGE_CONNECTION_STRING` | Development storage connection string pointed at the Floci Azure endpoint, carrying `BlobEndpoint`, `QueueEndpoint` and `TableEndpoint` and the well-known `devstoreaccount1` dev credentials | +For **Cosmos DB**, add `WithCosmosReference(azure)` / `withFlociAzureCosmosReference(azure)` — floci-az serves the Cosmos SQL/NoSQL API on the same endpoint, and this injects a Cosmos connection string so a Cosmos client resolves to the emulator: + +```csharp +var azure = builder.AddFlociAzure("floci-az"); + +builder.AddProject("api") + .WithReference(azure) // storage variables (optional) + .WithCosmosReference(azure) // ConnectionStrings__cosmos + .WaitFor(azure); +``` + +```typescript +const azure = await builder.addFlociAzure('floci-az'); + +await builder.addProject('api', '../MyApi/MyApi.csproj') + .withFlociAzureReference(azure) + .withFlociAzureCosmosReference(azure) + .waitFor(azure); +``` + +App side, this is the standard Aspire flow: + +```csharp +builder.AddAzureCosmosClient("cosmos"); +``` + +| Variable | Value | +|---|---| +| `ConnectionStrings__{connectionName}` (default `cosmos`) | `AccountEndpoint={scheme}://{host}:{port}/{account}-cosmos/;AccountKey=…` — the well-known Cosmos DB emulator key. `connectionName` and `accountName` (default `devstoreaccount1`) are configurable. | + +`WithCosmosReference` is additive — it injects only the Cosmos connection string, so combine it with `WithReference(azure)` when you also want the base endpoint / storage variables. (Talking to the floci Cosmos emulator over HTTP from the .NET SDK still needs the usual client-side settings — Gateway mode, and HTTP/1.1 — which are the app's concern, as with any local Cosmos emulator.) + + **GCP** ```csharp diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs index a3404c264..77c526e8e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs @@ -22,6 +22,10 @@ public static partial class FlociHostingExtension [AspireExport] public static ApplicationModel.IResourceBuilder WithConfigFile(this ApplicationModel.IResourceBuilder builder, string hostPath) { throw null; } + [AspireExport("withFlociAzureCosmosReference")] + public static ApplicationModel.IResourceBuilder WithCosmosReference(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder floci, string connectionName = "cosmos", string? accountName = null) + where TDestination : ApplicationModel.IResourceWithEnvironment { throw null; } + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs index 608ae3462..4fb98bf25 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs @@ -103,6 +103,78 @@ public async Task WithReferenceGcpSetsEmulatorHostEnvironmentVariables() Assert.StartsWith("{floci-gcp.bindings.gcp.scheme}://", storageHost); } + [Fact] + public async Task WithCosmosReferenceAzureSetsCosmosConnectionString() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + var floci = builder.AddFlociAzure("floci-az"); + var worker = builder.AddExecutable("worker", "dotnet", ".").WithCosmosReference(floci); + + var envVars = await ResolveEnvironmentAsync(builder, worker); + + Assert.Contains("ConnectionStrings__cosmos", envVars.Keys); + + var connectionString = Assert.IsType(envVars["ConnectionStrings__cosmos"]).ValueExpression; + Assert.Contains("AccountEndpoint=", connectionString); + Assert.Contains($"/{FlociAzureContainerResource.DefaultAccountName}-cosmos/;", connectionString); + Assert.Contains("AccountKey=", connectionString); + // The endpoint is an unresolved expression so it tracks Aspire's (possibly randomized) port + // assignment, and the scheme flips to https if a certificate is configured. + Assert.Contains("{floci-az.bindings.azure.scheme}://", connectionString); + } + + [Fact] + public async Task WithCosmosReferenceIsAdditiveAndComposesWithWithReference() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + var floci = builder.AddFlociAzure("floci-az"); + var worker = builder.AddExecutable("worker", "dotnet", ".") + .WithReference(floci) + .WithCosmosReference(floci); + + var envVars = await ResolveEnvironmentAsync(builder, worker); + + // The base storage reference and the Cosmos connection string coexist. + Assert.Contains("ConnectionStrings__floci-az", envVars.Keys); + Assert.Contains("AZURE_STORAGE_CONNECTION_STRING", envVars.Keys); + Assert.Contains("ConnectionStrings__cosmos", envVars.Keys); + } + + [Fact] + public async Task WithCosmosReferenceHonorsCustomConnectionAndAccountName() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + var floci = builder.AddFlociAzure("floci-az"); + var worker = builder.AddExecutable("worker", "dotnet", ".") + .WithCosmosReference(floci, connectionName: "notifications", accountName: "acct2"); + + var envVars = await ResolveEnvironmentAsync(builder, worker); + + Assert.Contains("ConnectionStrings__notifications", envVars.Keys); + var connectionString = Assert.IsType(envVars["ConnectionStrings__notifications"]).ValueExpression; + Assert.Contains("/acct2-cosmos/;", connectionString); + } + + [Fact] + public void WithCosmosReferenceBuilderShouldNotBeNull() + { + IResourceBuilder builder = null!; + IResourceBuilder floci = null!; + Assert.Throws(() => builder.WithCosmosReference(floci)); + } + + [Fact] + public void WithCosmosReferenceFlociResourceShouldNotBeNull() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + var worker = builder.AddContainer("worker", "myorg/worker"); + + Assert.Throws(() => worker.WithCosmosReference(null!)); + } + private static async Task> ResolveEnvironmentAsync( IDistributedApplicationBuilder builder, IResourceBuilder dependent) From 088933e01766dbbbcc8fbff6ba83b1ffc1208ce7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:44:10 +0100 Subject: [PATCH 2/2] refactor(floci): model Cosmos as child resource Use Aspire's standard WithReference flow and expose Cosmos in the resource graph. Revert the generated API surface because CI owns it. --- .../FlociAzureContainerResource.cs | 4 -- .../FlociAzureCosmosResource.cs | 51 +++++++++++++++++ .../FlociHostingExtension.Azure.cs | 57 +++++++------------ .../README.md | 14 +++-- .../CommunityToolkit.Aspire.Hosting.Floci.cs | 4 -- .../WithReferenceTests.cs | 45 +++++++++------ 6 files changed, 108 insertions(+), 67 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureCosmosResource.cs diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs index 5bb84f7d1..9286669a2 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureContainerResource.cs @@ -14,10 +14,6 @@ public class FlociAzureContainerResource(string name) : FlociContainerResource(n internal const string DefaultAccountName = "devstoreaccount1"; internal const string DefaultAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMh0=="; - // Well-known Cosmos DB emulator account key that floci-az accepts by default (no auth enforced). - internal const string DefaultCosmosAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - internal const string DefaultCosmosConnectionName = "cosmos"; - internal override void ApplyUIEnvironment(EnvironmentCallbackContext context) { context.EnvironmentVariables[FlociUIContainerResource.AzureEndpointEnvVar] = UIEndpointExpression; diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureCosmosResource.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureCosmosResource.cs new file mode 100644 index 000000000..5eb6be378 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureCosmosResource.cs @@ -0,0 +1,51 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents the Cosmos DB API exposed by a Floci Azure emulator resource. +/// +/// The name of the resource. +/// The Cosmos DB account name. +/// The parent Floci Azure emulator resource. +[AspireExport(ExposeProperties = true)] +public class FlociAzureCosmosResource( + string name, + string accountName, + FlociAzureContainerResource parent) : Resource(name), + IResourceWithParent, + IResourceWithConnectionString +{ + internal const string DefaultName = "cosmos"; + + // Well-known Cosmos DB emulator account key that floci-az accepts by default (no auth enforced). + internal const string DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + + /// + /// Gets the parent Floci Azure emulator resource. + /// + public FlociAzureContainerResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent)); + + /// + /// Gets the Cosmos DB account name. + /// + public string AccountName { get; } = string.IsNullOrWhiteSpace(accountName) + ? throw new ArgumentException("The account name cannot be empty or whitespace.", nameof(accountName)) + : accountName; + + /// + /// Gets the Cosmos DB account endpoint. + /// + public ReferenceExpression AccountEndpoint => + ReferenceExpression.Create($"{Parent.ConnectionStringExpression}/{AccountName}-cosmos/"); + + /// + /// Gets the Cosmos DB connection string expression. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"AccountEndpoint={AccountEndpoint};AccountKey={DefaultAccountKey};"); + + IEnumerable> IResourceWithConnectionString.GetConnectionProperties() => + Parent.CombineProperties([ + new("AccountEndpoint", AccountEndpoint), + new("AccountName", ReferenceExpression.Create($"{AccountName}")) + ]); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs index 784493844..90ec94c08 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs @@ -74,52 +74,35 @@ public static IResourceBuilder WithReference( }); /// - /// Adds a Cosmos DB connection string for the Floci Azure emulator to a dependent resource, - /// injecting ConnectionStrings__{connectionName} so a Cosmos client — e.g. - /// AddAzureCosmosClient(connectionName) — resolves to the emulator. floci-az serves the - /// Cosmos SQL/NoSQL API from the same endpoint under the {account}-cosmos path. + /// Adds a child resource representing the Cosmos DB API exposed by the Floci Azure emulator. /// /// - /// This is additive and composes with : - /// it injects only the Cosmos connection string, not the base ConnectionStrings__{name} - /// reference, so call both if you also want the base endpoint / storage variables. The endpoint - /// is left as an unresolved expression, so Aspire resolves it per dependent (a project gets - /// localhost:{hostPort}, a sibling container the container-network address) and a - /// certificate configured later still flips the scheme to https. + /// Reference the returned resource with Aspire's standard WithReference API to inject its + /// Cosmos DB connection string. floci-az serves the Cosmos SQL/NoSQL API from the parent + /// resource's endpoint under the {account}-cosmos path. /// - /// Adds a Cosmos DB connection string for the Floci Azure emulator - /// The type of the resource receiving the reference. - /// The resource builder for the resource receiving the reference. - /// The Floci Azure resource to reference. - /// The connection string name to inject (default: cosmos). + /// Adds a Cosmos DB child resource to the Floci Azure emulator + /// The Floci Azure resource builder. + /// The name of the Cosmos DB resource (default: cosmos). /// The Cosmos account name segment (default: devstoreaccount1). - /// A reference to the for further configuration. - [AspireExport("withFlociAzureCosmosReference")] - public static IResourceBuilder WithCosmosReference( - this IResourceBuilder builder, - IResourceBuilder floci, - string connectionName = FlociAzureContainerResource.DefaultCosmosConnectionName, + /// A reference to the for further configuration. + [AspireExport] + public static IResourceBuilder WithCosmos( + this IResourceBuilder builder, + [ResourceName] string name = FlociAzureCosmosResource.DefaultName, string? accountName = null) - where TDestination : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(floci); - ArgumentException.ThrowIfNullOrEmpty(connectionName); + ArgumentException.ThrowIfNullOrWhiteSpace(name); - FlociAzureContainerResource resource = floci.Resource; - string account = accountName ?? FlociAzureContainerResource.DefaultAccountName; + var cosmosResource = new FlociAzureCosmosResource( + name, + accountName ?? FlociAzureContainerResource.DefaultAccountName, + builder.Resource); - // Built inside the callback so the scheme is read after the whole AppHost is configured. - // The account key is the well-known Cosmos emulator key, distinct from the Azurite storage - // key used for AZURE_STORAGE_CONNECTION_STRING. - return builder.WithEnvironment(context => - { - ReferenceExpression accountEndpoint = ReferenceExpression.Create( - $"{resource.ConnectionStringExpression}/{account}-cosmos/"); - - context.EnvironmentVariables[$"ConnectionStrings__{connectionName}"] = ReferenceExpression.Create( - $"AccountEndpoint={accountEndpoint};AccountKey={FlociAzureContainerResource.DefaultCosmosAccountKey};"); - }); + return builder.ApplicationBuilder + .AddResource(cosmosResource) + .WithParentRelationship(builder); } /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md index 0cb4762c1..7c35d914e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md @@ -73,23 +73,25 @@ await builder.addProject('api', '../MyApi/MyApi.csproj') | `ConnectionStrings__floci-az` | `http://localhost:{port}` (standard Aspire connection string) | | `AZURE_STORAGE_CONNECTION_STRING` | Development storage connection string pointed at the Floci Azure endpoint, carrying `BlobEndpoint`, `QueueEndpoint` and `TableEndpoint` and the well-known `devstoreaccount1` dev credentials | -For **Cosmos DB**, add `WithCosmosReference(azure)` / `withFlociAzureCosmosReference(azure)` — floci-az serves the Cosmos SQL/NoSQL API on the same endpoint, and this injects a Cosmos connection string so a Cosmos client resolves to the emulator: +For **Cosmos DB**, use `WithCosmos()` / `withCosmos()` to model the Cosmos API as a child resource, then reference it through Aspire's standard connection-string flow: ```csharp var azure = builder.AddFlociAzure("floci-az"); +var cosmos = azure.WithCosmos(); builder.AddProject("api") - .WithReference(azure) // storage variables (optional) - .WithCosmosReference(azure) // ConnectionStrings__cosmos + .WithReference(azure) // storage variables (optional) + .WithReference(cosmos) // ConnectionStrings__cosmos .WaitFor(azure); ``` ```typescript const azure = await builder.addFlociAzure('floci-az'); +const cosmos = await azure.withCosmos(); await builder.addProject('api', '../MyApi/MyApi.csproj') .withFlociAzureReference(azure) - .withFlociAzureCosmosReference(azure) + .withReference(cosmos) .waitFor(azure); ``` @@ -101,9 +103,9 @@ builder.AddAzureCosmosClient("cosmos"); | Variable | Value | |---|---| -| `ConnectionStrings__{connectionName}` (default `cosmos`) | `AccountEndpoint={scheme}://{host}:{port}/{account}-cosmos/;AccountKey=…` — the well-known Cosmos DB emulator key. `connectionName` and `accountName` (default `devstoreaccount1`) are configurable. | +| `ConnectionStrings__{resourceName}` (default `cosmos`) | `AccountEndpoint={scheme}://{host}:{port}/{account}-cosmos/;AccountKey=…` — the well-known Cosmos DB emulator key. Resource name and account name (default `devstoreaccount1`) are configurable. | -`WithCosmosReference` is additive — it injects only the Cosmos connection string, so combine it with `WithReference(azure)` when you also want the base endpoint / storage variables. (Talking to the floci Cosmos emulator over HTTP from the .NET SDK still needs the usual client-side settings — Gateway mode, and HTTP/1.1 — which are the app's concern, as with any local Cosmos emulator.) +The Cosmos child resource is additive, so combine `WithReference(cosmos)` with `WithReference(azure)` when you also want the base endpoint / storage variables. (Talking to the floci Cosmos emulator over HTTP from the .NET SDK still needs the usual client-side settings — Gateway mode, and HTTP/1.1 — which are the app's concern, as with any local Cosmos emulator.) **GCP** diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs index 77c526e8e..a3404c264 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Floci/api/CommunityToolkit.Aspire.Hosting.Floci.cs @@ -22,10 +22,6 @@ public static partial class FlociHostingExtension [AspireExport] public static ApplicationModel.IResourceBuilder WithConfigFile(this ApplicationModel.IResourceBuilder builder, string hostPath) { throw null; } - [AspireExport("withFlociAzureCosmosReference")] - public static ApplicationModel.IResourceBuilder WithCosmosReference(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder floci, string connectionName = "cosmos", string? accountName = null) - where TDestination : ApplicationModel.IResourceWithEnvironment { throw null; } - [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } diff --git a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs index 4fb98bf25..d3fcb6cd4 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/WithReferenceTests.cs @@ -104,18 +104,26 @@ public async Task WithReferenceGcpSetsEmulatorHostEnvironmentVariables() } [Fact] - public async Task WithCosmosReferenceAzureSetsCosmosConnectionString() + public async Task WithCosmosCreatesChildResourceUsedByStandardWithReference() { IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); var floci = builder.AddFlociAzure("floci-az"); - var worker = builder.AddExecutable("worker", "dotnet", ".").WithCosmosReference(floci); + var cosmos = floci.WithCosmos(); + var worker = builder.AddExecutable("worker", "dotnet", ".").WithReference(cosmos); var envVars = await ResolveEnvironmentAsync(builder, worker); + Assert.Equal("cosmos", cosmos.Resource.Name); + Assert.Same(floci.Resource, cosmos.Resource.Parent); + Assert.Contains( + cosmos.Resource.Annotations.OfType(), + annotation => annotation.Type == "Parent" && ReferenceEquals(annotation.Resource, floci.Resource)); Assert.Contains("ConnectionStrings__cosmos", envVars.Keys); - var connectionString = Assert.IsType(envVars["ConnectionStrings__cosmos"]).ValueExpression; + var connectionStringReference = Assert.IsType(envVars["ConnectionStrings__cosmos"]); + Assert.Same(cosmos.Resource, connectionStringReference.Resource); + var connectionString = cosmos.Resource.ConnectionStringExpression.ValueExpression; Assert.Contains("AccountEndpoint=", connectionString); Assert.Contains($"/{FlociAzureContainerResource.DefaultAccountName}-cosmos/;", connectionString); Assert.Contains("AccountKey=", connectionString); @@ -125,14 +133,15 @@ public async Task WithCosmosReferenceAzureSetsCosmosConnectionString() } [Fact] - public async Task WithCosmosReferenceIsAdditiveAndComposesWithWithReference() + public async Task WithCosmosComposesWithFlociAzureReference() { IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); var floci = builder.AddFlociAzure("floci-az"); + var cosmos = floci.WithCosmos(); var worker = builder.AddExecutable("worker", "dotnet", ".") .WithReference(floci) - .WithCosmosReference(floci); + .WithReference(cosmos); var envVars = await ResolveEnvironmentAsync(builder, worker); @@ -143,38 +152,42 @@ public async Task WithCosmosReferenceIsAdditiveAndComposesWithWithReference() } [Fact] - public async Task WithCosmosReferenceHonorsCustomConnectionAndAccountName() + public async Task WithCosmosHonorsCustomResourceAndAccountName() { IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); var floci = builder.AddFlociAzure("floci-az"); - var worker = builder.AddExecutable("worker", "dotnet", ".") - .WithCosmosReference(floci, connectionName: "notifications", accountName: "acct2"); + var cosmos = floci.WithCosmos(name: "notifications", accountName: "acct2"); + var worker = builder.AddExecutable("worker", "dotnet", ".").WithReference(cosmos); var envVars = await ResolveEnvironmentAsync(builder, worker); Assert.Contains("ConnectionStrings__notifications", envVars.Keys); - var connectionString = Assert.IsType(envVars["ConnectionStrings__notifications"]).ValueExpression; + var connectionStringReference = Assert.IsType(envVars["ConnectionStrings__notifications"]); + Assert.Same(cosmos.Resource, connectionStringReference.Resource); + var connectionString = cosmos.Resource.ConnectionStringExpression.ValueExpression; Assert.Contains("/acct2-cosmos/;", connectionString); } [Fact] - public void WithCosmosReferenceBuilderShouldNotBeNull() + public void WithCosmosBuilderShouldNotBeNull() { - IResourceBuilder builder = null!; - IResourceBuilder floci = null!; - Assert.Throws(() => builder.WithCosmosReference(floci)); + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithCosmos()); } [Fact] - public void WithCosmosReferenceFlociResourceShouldNotBeNull() + public void WithCosmosResourceNameShouldNotBeEmpty() { IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); - var worker = builder.AddContainer("worker", "myorg/worker"); + var floci = builder.AddFlociAzure("floci-az"); - Assert.Throws(() => worker.WithCosmosReference(null!)); + Assert.Throws(() => floci.WithCosmos(GetInvalidResourceName())); } + private static string GetInvalidResourceName() => string.Empty; + private static async Task> ResolveEnvironmentAsync( IDistributedApplicationBuilder builder, IResourceBuilder dependent)