diff --git a/Directory.Packages.props b/Directory.Packages.props index a825f3f4ca6..fadc826b0d9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -70,6 +70,7 @@ + diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs index 5271a2fea5b..bf2cbff2c97 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs @@ -9,7 +9,7 @@ builder.AddProject("api") .WithExternalHttpEndpoints() - .WithReference(db); + .WithReference(db).WaitFor(db); #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging diff --git a/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj b/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj index 8c225ab9bbf..e0fa2341733 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj +++ b/src/Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.Azure.CosmosDB.csproj @@ -14,10 +14,13 @@ + + + diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 64fdf876c5d..91fb4fe2cdc 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -3,10 +3,14 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Aspire.Hosting.Azure.Cosmos; +using Azure.Identity; using Azure.Provisioning; using Azure.Provisioning.CosmosDB; using Azure.Provisioning.KeyVaults; using Azure.ResourceManager.CosmosDB.Models; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.CodeAnalysis; namespace Aspire.Hosting; @@ -69,9 +73,52 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis }; var resource = new AzureCosmosDBResource(name, configureConstruct); + + CosmosClient? cosmosClient = null; + + builder.Eventing.Subscribe(resource, async (@event, ct) => + { + var connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + + if (connectionString == null) + { + throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{resource.Name}' resource but the connection string was null."); + } + + cosmosClient = CreateCosmosClient(connectionString); + }); + + var healthCheckKey = $"{name}_check"; + builder.Services.AddHealthChecks().AddAzureCosmosDB(sp => + { + return cosmosClient ?? throw new InvalidOperationException("CosmosClient is not initialized."); + }, name: healthCheckKey); + return builder.AddResource(resource) .WithParameter(AzureBicepResource.KnownParameters.KeyVaultName) - .WithManifestPublishingCallback(resource.WriteToManifest); + .WithManifestPublishingCallback(resource.WriteToManifest) + .WithHealthCheck(healthCheckKey); + + static CosmosClient CreateCosmosClient(string connectionString) + { + var clientOptions = new CosmosClientOptions(); + clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = true; + + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + return new CosmosClient(uri.OriginalString, new DefaultAzureCredential(), clientOptions); + } + else + { + if (CosmosUtils.IsEmulatorConnectionString(connectionString)) + { + clientOptions.ConnectionMode = ConnectionMode.Gateway; + clientOptions.LimitToEndpoint = true; + } + + return new CosmosClient(connectionString, clientOptions); + } + } } /// diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs new file mode 100644 index 00000000000..9bd631a85cd --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Xunit; +using Xunit.Abstractions; + +namespace Aspire.Hosting.Azure.Tests; + +public class AzureCosmosDBEmulatorFunctionalTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + [RequiresDocker] + public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources() + { + // Cosmos can be pretty slow to spin up, lets give it plenty of time. + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + + var healthCheckTcs = new TaskCompletionSource(); + builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () => + { + return healthCheckTcs.Task; + }); + + var resource = builder.AddAzureCosmosDB("resource") + .RunAsEmulator() + .WithHealthCheck("blocking_check"); + + var dependentResource = builder.AddAzureCosmosDB("dependentresource") + .RunAsEmulator() + .WaitFor(resource); + + using var app = builder.Build(); + + var pendingStart = app.StartAsync(cts.Token); + + var rns = app.Services.GetRequiredService(); + + await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token); + + healthCheckTcs.SetResult(HealthCheckResult.Healthy()); + + await rns.WaitForResourceAsync(resource.Resource.Name, (re => re.Snapshot.HealthStatus == HealthStatus.Healthy), cts.Token); + + await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token); + + await pendingStart; + + await app.StopAsync(); + } + +}