Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents the Cosmos DB API exposed by a Floci Azure emulator resource.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="accountName">The Cosmos DB account name.</param>
/// <param name="parent">The parent Floci Azure emulator resource.</param>
[AspireExport(ExposeProperties = true)]
public class FlociAzureCosmosResource(
string name,
string accountName,
FlociAzureContainerResource parent) : Resource(name),
IResourceWithParent<FlociAzureContainerResource>,
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==";

/// <summary>
/// Gets the parent Floci Azure emulator resource.
/// </summary>
public FlociAzureContainerResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));

/// <summary>
/// Gets the Cosmos DB account name.
/// </summary>
public string AccountName { get; } = string.IsNullOrWhiteSpace(accountName)
? throw new ArgumentException("The account name cannot be empty or whitespace.", nameof(accountName))
: accountName;

/// <summary>
/// Gets the Cosmos DB account endpoint.
/// </summary>
public ReferenceExpression AccountEndpoint =>
ReferenceExpression.Create($"{Parent.ConnectionStringExpression}/{AccountName}-cosmos/");

/// <summary>
/// Gets the Cosmos DB connection string expression.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"AccountEndpoint={AccountEndpoint};AccountKey={DefaultAccountKey};");

IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties() =>
Parent.CombineProperties([
new("AccountEndpoint", AccountEndpoint),
new("AccountName", ReferenceExpression.Create($"{AccountName}"))
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(

});

/// <summary>
/// Adds a child resource representing the Cosmos DB API exposed by the Floci Azure emulator.
/// </summary>
/// <remarks>
/// Reference the returned resource with Aspire's standard <c>WithReference</c> API to inject its
/// Cosmos DB connection string. floci-az serves the Cosmos SQL/NoSQL API from the parent
/// resource's endpoint under the <c>{account}-cosmos</c> path.
/// </remarks>
/// <ats-summary>Adds a Cosmos DB child resource to the Floci Azure emulator</ats-summary>
/// <param name="builder">The Floci Azure resource builder.</param>
/// <param name="name">The name of the Cosmos DB resource (default: <c>cosmos</c>).</param>
/// <param name="accountName">The Cosmos account name segment (default: <c>devstoreaccount1</c>).</param>
/// <returns>A reference to the <see cref="IResourceBuilder{FlociAzureCosmosResource}"/> for further configuration.</returns>
[AspireExport]
public static IResourceBuilder<FlociAzureCosmosResource> WithCosmos(
this IResourceBuilder<FlociAzureContainerResource> builder,
[ResourceName] string name = FlociAzureCosmosResource.DefaultName,
string? accountName = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(name);

var cosmosResource = new FlociAzureCosmosResource(
name,
accountName ?? FlociAzureContainerResource.DefaultAccountName,
builder.Resource);

return builder.ApplicationBuilder
.AddResource(cosmosResource)
.WithParentRelationship(builder);
}

/// <summary>
/// Mounts the Docker socket into the Floci Azure container so that Azure Functions and other
/// container-backed services can launch sibling containers.
Expand Down
35 changes: 35 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Floci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@ 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**, 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<MyApi>("api")
.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)
.withReference(cosmos)
.waitFor(azure);
```

App side, this is the standard Aspire flow:

```csharp
builder.AddAzureCosmosClient("cosmos");
```

| Variable | Value |
|---|---|
| `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. |

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**

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,91 @@ public async Task WithReferenceGcpSetsEmulatorHostEnvironmentVariables()
Assert.StartsWith("{floci-gcp.bindings.gcp.scheme}://", storageHost);
}

[Fact]
public async Task WithCosmosCreatesChildResourceUsedByStandardWithReference()
{
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();

var floci = builder.AddFlociAzure("floci-az");
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<ResourceRelationshipAnnotation>(),
annotation => annotation.Type == "Parent" && ReferenceEquals(annotation.Resource, floci.Resource));
Assert.Contains("ConnectionStrings__cosmos", envVars.Keys);

var connectionStringReference = Assert.IsType<ConnectionStringReference>(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);
// 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 WithCosmosComposesWithFlociAzureReference()
{
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();

var floci = builder.AddFlociAzure("floci-az");
var cosmos = floci.WithCosmos();
var worker = builder.AddExecutable("worker", "dotnet", ".")
.WithReference(floci)
.WithReference(cosmos);

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 WithCosmosHonorsCustomResourceAndAccountName()
{
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();

var floci = builder.AddFlociAzure("floci-az");
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 connectionStringReference = Assert.IsType<ConnectionStringReference>(envVars["ConnectionStrings__notifications"]);
Assert.Same(cosmos.Resource, connectionStringReference.Resource);
var connectionString = cosmos.Resource.ConnectionStringExpression.ValueExpression;
Assert.Contains("/acct2-cosmos/;", connectionString);
}

[Fact]
public void WithCosmosBuilderShouldNotBeNull()
{
IResourceBuilder<FlociAzureContainerResource> builder = null!;

Assert.Throws<ArgumentNullException>(() => builder.WithCosmos());
}

[Fact]
public void WithCosmosResourceNameShouldNotBeEmpty()
{
IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
var floci = builder.AddFlociAzure("floci-az");

Assert.Throws<ArgumentException>(() => floci.WithCosmos(GetInvalidResourceName()));
}

private static string GetInvalidResourceName() => string.Empty;

private static async Task<Dictionary<string, object>> ResolveEnvironmentAsync<T>(
IDistributedApplicationBuilder builder,
IResourceBuilder<T> dependent)
Expand Down
Loading