From 78837be6f54074827b23034ceff7e074615c6c58 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 14 Jan 2025 10:36:29 -0600 Subject: [PATCH] Add support for CosmosDB vnext-preview emulator (#7048) * Add support for CosmosDB vnext-preview emulator Added a new experimental API - RunAsPreviewEmulator. This will use the new Linux-based emulator, which starts faster. And it also has support for a built-in Data Explorer which can be enabled by calling WithDataExplorer on the emulator. Fix #5163 * Add tests and fix WithDataVolume for the preview emulator. Also fixing the EF CosmosDB code to specify the PartitionKey. * Add wait for healthy in cosmos tests. * Skip the new Cosmos emulator tests until the emulator is more stable. * Disable VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources for the preview emulator --- .../CosmosEndToEnd.ApiService/Program.cs | 8 +- .../CosmosEndToEnd.AppHost/Program.cs | 4 +- .../AzureCosmosDBEmulatorConnectionString.cs | 5 +- .../AzureCosmosDBEmulatorResource.cs | 7 +- .../AzureCosmosDBExtensions.cs | 63 ++++++++++++++-- .../AzureCosmosDBResource.cs | 7 +- .../CosmosDBEmulatorContainerImageTags.cs | 3 + .../PublicAPI.Shipped.txt | 1 - .../PublicAPI.Unshipped.txt | 2 + .../AzureCosmosDBEmulatorFunctionalTests.cs | 74 +++++++++++++++---- .../AzureResourceExtensionsTests.cs | 19 +++++ 11 files changed, 163 insertions(+), 30 deletions(-) diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs index 7aec7379ff..a6b1bcaf74 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs @@ -20,7 +20,7 @@ app.MapGet("/", async (CosmosClient cosmosClient) => { var db = (await cosmosClient.CreateDatabaseIfNotExistsAsync("db")).Database; - var container = (await db.CreateContainerIfNotExistsAsync("entries", "/Id")).Container; + var container = (await db.CreateContainerIfNotExistsAsync("entries", "/id")).Container; // Add an entry to the database on each request. var newEntry = new Entry() { Id = Guid.NewGuid().ToString() }; @@ -69,6 +69,12 @@ public class Entry public class TestCosmosContext(DbContextOptions options) : DbContext(options) { public DbSet Entries { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasPartitionKey(e => e.Id); + } } public class EntityFrameworkEntry diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs index bf2cbff2c9..5e8cc18007 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs @@ -3,9 +3,11 @@ var builder = DistributedApplication.CreateBuilder(args); +#pragma warning disable ASPIRECOSMOS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var db = builder.AddAzureCosmosDB("cosmos") .AddDatabase("db") - .RunAsEmulator(); + .RunAsPreviewEmulator(e => e.WithDataExplorer()); +#pragma warning restore ASPIRECOSMOS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. builder.AddProject("api") .WithExternalHttpEndpoints() diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs index 9faee561a4..f8cd05afe8 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorConnectionString.cs @@ -8,5 +8,8 @@ namespace Aspire.Hosting.Azure; internal static class AzureCosmosDBEmulatorConnectionString { - public static ReferenceExpression Create(EndpointReference endpoint) => ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint=https://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)};DisableServerCertificateValidation=True;"); + public static ReferenceExpression Create(EndpointReference endpoint, bool isPreviewEmulator) => + isPreviewEmulator + ? ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint={endpoint.Property(EndpointProperty.Url)}") + : ReferenceExpression.Create($"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint=https://{endpoint.Property(EndpointProperty.IPV4Host)}:{endpoint.Property(EndpointProperty.Port)};DisableServerCertificateValidation=True;"); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs index 1eacd90e27..d420fa3b45 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBEmulatorResource.cs @@ -11,11 +11,8 @@ namespace Aspire.Hosting.Azure; /// The inner resource used to store annotations. public class AzureCosmosDBEmulatorResource(AzureCosmosDBResource innerResource) : ContainerResource(innerResource.Name), IResource { - private readonly AzureCosmosDBResource _innerResource = innerResource; - - /// - public override string Name => _innerResource.Name; + internal AzureCosmosDBResource InnerResource { get; } = innerResource; /// - public override ResourceAnnotationCollection Annotations => _innerResource.Annotations; + public override ResourceAnnotationCollection Annotations => InnerResource.Annotations; } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index dd73459e00..613651079b 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Aspire.Hosting.Azure.Cosmos; @@ -12,7 +14,6 @@ using Azure.Provisioning.KeyVault; using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; -using System.Globalization; namespace Aspire.Hosting; @@ -108,18 +109,36 @@ public static IResourceBuilder AddAzureCosmosDB(this IDis /// This version of the package defaults to the tag of the / container image. /// public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + => builder.RunAsEmulator(configureContainer, useVNextPreview: false); + + /// + /// Configures an Azure Cosmos DB resource to be emulated using the Azure Cosmos DB Linux-based emulator (preview) with the NoSQL API. This resource requires an to be added to the application model. + /// For more information on the Azure Cosmos DB emulator, see . + /// + /// The Azure Cosmos DB resource builder. + /// Callback that exposes underlying container used for emulation to allow for customization. + /// A reference to the . + /// + /// This version of the package defaults to the tag of the / container image. + /// + [Experimental("ASPIRECOSMOS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] + public static IResourceBuilder RunAsPreviewEmulator(this IResourceBuilder builder, Action>? configureContainer = null) + => builder.RunAsEmulator(configureContainer, useVNextPreview: true); + + private static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, Action>? configureContainer, bool useVNextPreview) { if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode) { return builder; } - builder.WithEndpoint(name: "emulator", targetPort: 8081) + var scheme = useVNextPreview ? "http" : null; + builder.WithEndpoint(name: "emulator", scheme: scheme, targetPort: 8081) .WithAnnotation(new ContainerImageAnnotation { Registry = CosmosDBEmulatorContainerImageTags.Registry, Image = CosmosDBEmulatorContainerImageTags.Image, - Tag = CosmosDBEmulatorContainerImageTags.Tag + Tag = useVNextPreview ? CosmosDBEmulatorContainerImageTags.TagVNextPreview : CosmosDBEmulatorContainerImageTags.Tag }); CosmosClient? cosmosClient = null; @@ -182,8 +201,12 @@ static CosmosClient CreateCosmosClient(string connectionString) /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. /// A builder for the . public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null) - => builder.WithEnvironment("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "true") - .WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/tmp/cosmos/appdata", isReadOnly: false); + { + var dataPath = builder.Resource.InnerResource.IsPreviewEmulator ? "/data": "/tmp/cosmos/appdata"; + + return builder.WithEnvironment("AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE", "true") + .WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), dataPath, isReadOnly: false); + } /// /// Configures the gateway port for the Azure Cosmos DB emulator. @@ -210,6 +233,11 @@ public static IResourceBuilder WithGatewayPort(th /// public static IResourceBuilder WithPartitionCount(this IResourceBuilder builder, int count) { + if (builder.Resource.InnerResource.IsPreviewEmulator) + { + throw new NotSupportedException($"'{nameof(WithPartitionCount)}' does not work when using the preview version of the Azure Cosmos DB emulator."); + } + if (count < 1 || count > 250) { throw new ArgumentOutOfRangeException(nameof(count), count, "Count must be between 1 and 250."); @@ -229,4 +257,29 @@ public static IResourceBuilder AddDatabase(this IResource builder.Resource.Databases.Add(databaseName); return builder; } + + /// + /// Configures the Azure Cosmos DB preview emulator to expose the Data Explorer endpoint. + /// + /// Builder for the Cosmos emulator container + /// Optional host port to bind the Data Explorer to. + /// Cosmos emulator resource builder. + /// + /// The Data Explorer is only available with . + /// + [Experimental("ASPIRECOSMOS001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] + public static IResourceBuilder WithDataExplorer(this IResourceBuilder builder, int? port = null) + { + if (!builder.Resource.InnerResource.IsPreviewEmulator) + { + throw new NotSupportedException($"The Data Explorer endpoint is only available when using the preview version of the Azure Cosmos DB emulator. Call '{nameof(RunAsPreviewEmulator)}' instead."); + } + + return builder.WithEndpoint(endpointName: "data-explorer", endpoint => + { + endpoint.UriScheme = "http"; + endpoint.TargetPort = 1234; + endpoint.Port = port; + }); + } } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index e829476fc4..94b1691a0e 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Aspire.Hosting.Azure.Cosmos; namespace Aspire.Hosting; @@ -28,12 +29,16 @@ public class AzureCosmosDBResource(string name, Action public bool IsEmulator => this.IsContainer(); + internal bool IsPreviewEmulator => + this.TryGetContainerImageName(out var imageName) && + imageName == $"{CosmosDBEmulatorContainerImageTags.Registry}/{CosmosDBEmulatorContainerImageTags.Image}:{CosmosDBEmulatorContainerImageTags.TagVNextPreview}"; + /// /// Gets the connection string template for the manifest for the Azure Cosmos DB resource. /// public ReferenceExpression ConnectionStringExpression => IsEmulator - ? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint) + ? AzureCosmosDBEmulatorConnectionString.Create(EmulatorEndpoint, IsPreviewEmulator) : ReferenceExpression.Create($"{ConnectionString}"); } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs b/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs index 76be2a184c..7ca91640b6 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/CosmosDBEmulatorContainerImageTags.cs @@ -13,4 +13,7 @@ internal static class CosmosDBEmulatorContainerImageTags /// latest public const string Tag = "latest"; + + /// vnext-preview + public const string TagVNextPreview = "vnext-preview"; } diff --git a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt index 53ac0afce6..949dee6263 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt +++ b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Shipped.txt @@ -8,7 +8,6 @@ Aspire.Hosting.AzureCosmosDBResource.ConnectionStringExpression.get -> Aspire.Ho Aspire.Hosting.AzureCosmosDBResource.IsEmulator.get -> bool Aspire.Hosting.AzureCosmosExtensions override Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource.Annotations.get -> Aspire.Hosting.ApplicationModel.ResourceAnnotationCollection! -override Aspire.Hosting.Azure.AzureCosmosDBEmulatorResource.Name.get -> string! static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! databaseName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt index d72fe00b8d..a830d73815 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting.Azure.CosmosDB/PublicAPI.Unshipped.txt @@ -2,5 +2,7 @@ *REMOVED*static Aspire.Hosting.AzureCosmosExtensions.AddAzureCosmosDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action!, Aspire.Hosting.ResourceModuleConstruct!, Azure.Provisioning.CosmosDB.CosmosDBAccount!, System.Collections.Generic.IEnumerable!>? configureResource) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! *REMOVED*Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action! configureConstruct) -> void Aspire.Hosting.AzureCosmosDBResource.AzureCosmosDBResource(string! name, System.Action! configureInfrastructure) -> void +static Aspire.Hosting.AzureCosmosExtensions.RunAsPreviewEmulator(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action!>? configureContainer = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.AzureCosmosExtensions.WithDataExplorer(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.AzureCosmosExtensions.WithPartitionCount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, int count) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs index 55e22117f7..d1156b9b1b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBEmulatorFunctionalTests.cs @@ -18,9 +18,11 @@ namespace Aspire.Hosting.Azure.Tests; public class AzureCosmosDBEmulatorFunctionalTests(ITestOutputHelper testOutputHelper) { - [Fact] + [Theory] + // [InlineData(true)] // "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820" + [InlineData(false)] [RequiresDocker] - public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources() + public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources(bool usePreview) { // Cosmos can be pretty slow to spin up, lets give it plenty of time. var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); @@ -33,7 +35,7 @@ public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources() }); var resource = builder.AddAzureCosmosDB("resource") - .RunAsEmulator() + .RunAsEmulator(usePreview) .WithHealthCheck("blocking_check"); var dependentResource = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") @@ -60,9 +62,11 @@ public async Task VerifyWaitForOnCosmosDBEmulatorBlocksDependentResources() await app.StopAsync(); } - [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] + [Theory(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] + [InlineData(true)] + [InlineData(false)] [RequiresDocker(Reason = "CosmosDB emulator is needed for this test")] - public async Task VerifyCosmosResource() + public async Task VerifyCosmosResource(bool usePreview) { var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); var pipeline = new ResiliencePipelineBuilder() @@ -83,12 +87,15 @@ public async Task VerifyCosmosResource() var cosmos = builder.AddAzureCosmosDB("cosmos"); var db = cosmos.AddDatabase(databaseName) - .RunAsEmulator(); + .RunAsEmulator(usePreview); using var app = builder.Build(); await app.StartAsync(); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceHealthyAsync(db.Resource.Name, cts.Token); + var hb = Host.CreateApplicationBuilder(); hb.Configuration[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default); hb.AddAzureCosmosClient(db.Resource.Name); @@ -107,12 +114,16 @@ await pipeline.ExecuteAsync(async token => { Database database = await cosmosClient.CreateDatabaseIfNotExistsAsync(databaseName, cancellationToken: token); Container container = await database.CreateContainerIfNotExistsAsync(containerName, "/id", cancellationToken: token); - var query = new QueryDefinition("SELECT VALUE 1"); - var results = await container.GetItemQueryIterator(query).ReadNextAsync(token); + var testObject = new { id = "1", data = "assertionValue" }; + await container.CreateItemAsync(testObject, cancellationToken: token); + + // run query and check the value + QueryDefinition query = new("SELECT VALUE c.data FROM c WHERE c.id = '1'"); + var results = await container.GetItemQueryIterator(query).ReadNextAsync(token); Assert.True(results.Count == 1); - Assert.True(results.First() == 1); + Assert.True(results.First() == testObject.data); await dbContext.Database.EnsureCreatedAsync(token); dbContext.AddRange([new Entry(), new Entry()]); @@ -121,9 +132,11 @@ await pipeline.ExecuteAsync(async token => }, cts.Token); } - [Fact(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] + [Theory(Skip = "Using CosmosDB emulator in integration tests leads to flaky tests - https://github.com/dotnet/aspire/issues/5820")] + [InlineData(true)] + [InlineData(false)] [RequiresDocker] - public async Task WithDataVolumeShouldPersistStateBetweenUsages() + public async Task WithDataVolumeShouldPersistStateBetweenUsages(bool usePreview) { // Use a volume to do a snapshot save @@ -148,7 +161,7 @@ public async Task WithDataVolumeShouldPersistStateBetweenUsages() var volumeName = VolumeNameGenerator.Generate(cosmos1, nameof(WithDataVolumeShouldPersistStateBetweenUsages)); var db1 = cosmos1.AddDatabase(databaseName) - .RunAsEmulator(emulator => emulator.WithDataVolume(volumeName)); + .RunAsEmulator(usePreview, volumeName); // if the volume already exists (because of a crashing previous run), delete it DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true); @@ -159,6 +172,9 @@ public async Task WithDataVolumeShouldPersistStateBetweenUsages() { await app.StartAsync(); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceHealthyAsync(db1.Resource.Name, cts.Token); + try { var hb = Host.CreateApplicationBuilder(); @@ -185,7 +201,6 @@ await pipeline.ExecuteAsync(async token => await container.CreateItemAsync(testObject, cancellationToken: token); }, cts.Token); - } } finally @@ -199,12 +214,15 @@ await pipeline.ExecuteAsync(async token => var cosmos2 = builder2.AddAzureCosmosDB("cosmos"); var db2 = cosmos2.AddDatabase(databaseName) - .RunAsEmulator(emulator => emulator.WithDataVolume(volumeName)); + .RunAsEmulator(usePreview, volumeName); using (var app = builder2.Build()) { await app.StartAsync(); + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceHealthyAsync(db2.Resource.Name, cts.Token); + try { var hb = Host.CreateApplicationBuilder(); @@ -228,7 +246,7 @@ await pipeline.ExecuteAsync(async token => { var container = cosmosClient.GetContainer(databaseName, containerName); - QueryDefinition query = new("SELECT VALUE data FROM c WHERE c.id = '1'"); + QueryDefinition query = new("SELECT VALUE c.data FROM c WHERE c.id = '1'"); // run query and check the value var results = await container.GetItemQueryIterator(query).ReadNextAsync(token); @@ -253,9 +271,35 @@ await pipeline.ExecuteAsync(async token => public class EFCoreCosmosDbContext(DbContextOptions options) : DbContext(options) { public DbSet Entries { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasPartitionKey(e => e.Id); + } } public record Entry { public Guid Id { get; set; } = Guid.NewGuid(); } + +internal static class CosmosExtensions +{ + public static IResourceBuilder RunAsEmulator(this IResourceBuilder builder, bool usePreview, string? volumeName = null) + { + void WithVolume(IResourceBuilder emulator) + { + if (volumeName is not null) + { + emulator.WithDataVolume(volumeName); + } + } + + return usePreview +#pragma warning disable ASPIRECOSMOS001 // RunAsPreviewEmulator is experimental + ? builder.RunAsPreviewEmulator(WithVolume) +#pragma warning restore ASPIRECOSMOS001 // RunAsPreviewEmulator is experimental + : builder.RunAsEmulator(WithVolume); + } +} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourceExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourceExtensionsTests.cs index 3295dcb1b1..9a8a9d9af0 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourceExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourceExtensionsTests.cs @@ -190,4 +190,23 @@ public async Task AddAzureCosmosDBWithPartitionCountCanOverrideNumberOfPartition Assert.Equal(partitionCount.ToString(CultureInfo.InvariantCulture), config["AZURE_COSMOS_EMULATOR_PARTITION_COUNT"]); } + + [Fact] + public void AddAzureCosmosDBWithDataExplorer() + { +#pragma warning disable ASPIRECOSMOS001 // RunAsPreviewEmulator is experimental + using var builder = TestDistributedApplicationBuilder.Create(); + + var cosmos = builder.AddAzureCosmosDB("cosmos"); + cosmos.RunAsPreviewEmulator(e => e.WithDataExplorer()); + + var endpoint = cosmos.GetEndpoint("data-explorer"); + Assert.NotNull(endpoint); + Assert.Equal(1234, endpoint.TargetPort); + + // WithDataExplorer doesn't work against the non-preview emulator + var cosmos2 = builder.AddAzureCosmosDB("cosmos2"); + Assert.Throws(() => cosmos2.RunAsEmulator(e => e.WithDataExplorer())); +#pragma warning restore ASPIRECOSMOS001 // RunAsPreviewEmulator is experimental + } }