diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
index ae41bd9a085..53b14c3097a 100644
--- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
+++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
@@ -7,6 +7,10 @@
Azure resource types for .NET Aspire.
+
+
+
+
diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs
index 009dc8652da..292b886768b 100644
--- a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs
@@ -50,15 +50,16 @@ private static void WriteCosmosDBDatabaseToManifest(ManifestPublishingContext co
private static void WriteCosmosDBToManifest(ManifestPublishingContext context, AzureCosmosDBResource cosmosDb)
{
- var connectionString = cosmosDb.GetConnectionString();
- if (connectionString is null)
+ // If we are using an emulator then we assume that a connection string was not
+ // provided for the purpose of manifest generation.
+ if (cosmosDb.IsEmulator || cosmosDb.GetConnectionString() is not { } connectionString)
{
context.Writer.WriteString("type", "azure.cosmosdb.account.v0");
}
else
{
context.Writer.WriteString("type", "azure.cosmosdb.connection.v0");
- context.Writer.WriteString("connectionString", cosmosDb.GetConnectionString());
+ context.Writer.WriteString("connectionString", connectionString);
}
}
diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs
index a30e210b0be..5a1159ba1ba 100644
--- a/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs
@@ -3,6 +3,7 @@
using System.Collections.ObjectModel;
using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure.Cosmos;
namespace Aspire.Hosting.Azure.Data.Cosmos;
@@ -58,13 +59,5 @@ internal void AddDatabase(AzureCosmosDBDatabaseResource database)
file static class AzureCosmosDBEmulatorConnectionString
{
- ///
- /// Gets the well-known and documented Azure Cosmos DB emulator account key.
- /// See
- ///
- private const string ConnectionStringHeader = """
- AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;
- """;
-
- public static string Create(int port) => $"{ConnectionStringHeader}AccountEndpoint=https://127.0.0.1:{port};";
+ public static string Create(int port) => $"AccountKey={CosmosConstants.EmulatorAccountKey};AccountEndpoint=https://127.0.0.1:{port};";
}
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj b/src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj
index 267cf842893..3e37dbfb3a2 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj
@@ -11,8 +11,10 @@
+
+
-
+
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
index cfe43cbc4e0..75548a9dad5 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Aspire.Hosting.Azure.Cosmos;
using Aspire.Microsoft.Azure.Cosmos;
using Azure.Identity;
using Microsoft.Azure.Cosmos;
@@ -92,6 +93,16 @@ private static void AddAzureCosmosDB(
});
}
+ if (settings.IgnoreEmulatorCertificate && CosmosUtils.IsEmulatorConnectionString(settings.ConnectionString))
+ {
+ clientOptions.HttpClientFactory = () => new HttpClient(new HttpClientHandler()
+ {
+ ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ });
+ clientOptions.ConnectionMode = ConnectionMode.Gateway;
+ clientOptions.LimitToEndpoint = true;
+ }
+
configureClientOptions?.Invoke(clientOptions);
if (serviceKey is null)
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs
index 346d3b60d03..4d9d4a74d27 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs
@@ -37,5 +37,10 @@ public sealed class AzureCosmosDBSettings
/// Gets or sets the credential used to authenticate to the Azure Cosmos DB endpoint.
///
public TokenCredential? Credential { get; set; }
+
+ ///
+ /// Controls whether the Cosmos DB emulator certificate is ignored when its use is detected.
+ ///
+ public bool IgnoreEmulatorCertificate { get; set; }
}
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json
index ba3eece0720..d2f41cc38dc 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json
@@ -30,6 +30,10 @@
"type": "string",
"description": "Gets or sets the connection string of the Azure Cosmos database to connect to."
},
+ "IgnoreEmulatorCertificate": {
+ "type": "boolean",
+ "description": "Controls whether the Cosmos DB emulator certificate is ignored when its use is detected."
+ },
"Tracing": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.",
diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md b/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md
index 2a0b06a557a..7947b13a368 100644
--- a/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md
+++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md
@@ -131,6 +131,25 @@ The `AddAzureCosmosDB` method will read connection information from the AppHost'
builder.AddAzureCosmosDB("cosmosdb");
```
+### Emulator usage
+
+Aspire supports the usage of the Azure Cosmos DB emulator to use the emulator, add the following to your AppHost project:
+
+```csharp
+// AppHost
+var cosmosdb = builder.AddAzureCosmosDB("cosmos").UseEmulator();
+```
+
+When the AppHost starts up a local container running the Azure CosmosDB will also be started. Inside the project that uses CosmosDB you can specify that you want to ignore the server certificate, so you don't need to manually download and install it:
+
+```csharp
+// Service code
+builder.AddCosmosDB("cosmos", (settings) =>
+{
+ settings.IgnoreEmulatorCertificate = true;
+});
+```
+
## Additional documentation
* https://learn.microsoft.com/azure/cosmos-db/nosql/sdk-dotnet-v3
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj
index 021a662fea3..ae9877f56cb 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs
index fa518a0a41f..beba4ea2956 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs
@@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
+using Aspire.Hosting.Azure.Cosmos;
using Aspire.Microsoft.EntityFrameworkCore.Cosmos;
using Azure.Identity;
+using Microsoft.Azure.Cosmos;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Configuration;
@@ -127,6 +129,16 @@ void UseCosmosBody(CosmosDbContextOptionsBuilder builder)
{
builder.Region(settings.Region);
}
+
+ if (settings.IgnoreEmulatorCertificate && CosmosUtils.IsEmulatorConnectionString(settings.ConnectionString))
+ {
+ builder.HttpClientFactory(() => new HttpClient(new HttpClientHandler()
+ {
+ ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ }));
+ builder.ConnectionMode(ConnectionMode.Gateway);
+ builder.LimitToEndpoint(true);
+ }
}
}
}
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
index 338e52012f0..d6b4611a271 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json
@@ -52,6 +52,10 @@
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the DbContext will be pooled or explicitly created every time it's requested."
},
+ "IgnoreEmulatorCertificate": {
+ "type": "boolean",
+ "description": "Controls whether the Cosmos DB emulator certificate is ignored when its use is detected."
+ },
"Metrics": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.",
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs
index 2894a04ce7b..4568156dc95 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs
@@ -55,4 +55,9 @@ public sealed class EntityFrameworkCoreCosmosDBSettings
/// Gets or sets a string value that indicates what Azure region this client will run in.
///
public string? Region { get; set; }
+
+ ///
+ /// Controls whether the Cosmos DB emulator certificate is ignored when its use is detected.
+ ///
+ public bool IgnoreEmulatorCertificate { get; set; }
}
diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md
index ac24bd279cc..101ed8f1e68 100644
--- a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md
+++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md
@@ -109,6 +109,26 @@ The `AddAzureCosmosDB` method will read connection information from the AppHost'
builder.AddCosmosDbContext("cosmosdb");
```
+### Emulator usage
+
+Aspire supports the usage of the Azure Cosmos DB emulator to use the emulator, add the following to your AppHost project:
+
+```csharp
+// AppHost
+var cosmosdb = builder.AddAzureCosmosDB("cosmos").UseEmulator();
+```
+
+When the AppHost starts up a local container running the Azure CosmosDB will also be started. Inside the project that uses CosmosDB you also need to specify that you want to ignore the server certificate (so you don't need to manually download and install it):
+
+```csharp
+// Service code
+builder.AddCosmosDbContext("cosmos", "mydb", (settings) =>
+{
+ settings.IgnoreEmulatorCertificate = true;
+});
+
+```
+
## Additional documentation
* https://learn.microsoft.com/ef/core/
diff --git a/src/Shared/Cosmos/CosmosConstants.cs b/src/Shared/Cosmos/CosmosConstants.cs
new file mode 100644
index 00000000000..8665266577e
--- /dev/null
+++ b/src/Shared/Cosmos/CosmosConstants.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.Azure.Cosmos;
+
+internal static class CosmosConstants
+{
+ ///
+ /// Gets the well-known and documented Azure Cosmos DB emulator account key.
+ /// See
+ ///
+ internal const string EmulatorAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
+}
diff --git a/src/Shared/Cosmos/CosmosUtils.cs b/src/Shared/Cosmos/CosmosUtils.cs
new file mode 100644
index 00000000000..305e7b3adca
--- /dev/null
+++ b/src/Shared/Cosmos/CosmosUtils.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Data.Common;
+
+namespace Aspire.Hosting.Azure.Cosmos;
+
+internal static class CosmosUtils
+{
+ internal static bool IsEmulatorConnectionString(string? connectionString)
+ {
+ if (connectionString == null)
+ {
+ return false;
+ }
+
+ var builder = new DbConnectionStringBuilder();
+ builder.ConnectionString = connectionString;
+ var accountKeyFromConnectionString = builder["AccountKey"].ToString();
+ return accountKeyFromConnectionString == CosmosConstants.EmulatorAccountKey;
+ }
+}
diff --git a/tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs
new file mode 100644
index 00000000000..0e99ca4f585
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.Tests.Helpers;
+using Polly;
+using Polly.Retry;
+using Xunit;
+
+namespace Aspire.Hosting.Tests.Cosmos;
+
+[Collection("IntegrationServices")]
+public class CosmosFunctionalTests
+{
+ private readonly IntegrationServicesFixture _integrationServicesFixture;
+
+ public CosmosFunctionalTests(IntegrationServicesFixture integrationServicesFixture)
+ {
+ _integrationServicesFixture = integrationServicesFixture;
+ }
+
+ [LocalOnlyFact()]
+ public async Task VerifyCosmosWorks()
+ {
+ var testProgram = _integrationServicesFixture.TestProgram;
+ var client = _integrationServicesFixture.HttpClient;
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(6));
+
+ await RetryPolicy.Handle()
+ .WaitAndRetryAsync(20, (count) => TimeSpan.FromSeconds(15))
+ .ExecuteAsync(async () =>
+ {
+ var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/cosmos/verify", cts.Token);
+ response.EnsureSuccessStatusCode();
+
+ var responseContent = await response.Content.ReadAsStringAsync();
+ Assert.True(response.IsSuccessStatusCode, responseContent);
+ });
+ }
+}
diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs
index e9f9f2646fb..77ad3aefcfa 100644
--- a/tests/testproject/TestProject.AppHost/TestProgram.cs
+++ b/tests/testproject/TestProject.AppHost/TestProgram.cs
@@ -63,6 +63,8 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer
var oracleDatabaseAbstract = AppBuilder.AddOracleDatabaseContainer("oracledatabaseabstract");
var kafkaAbstract = AppBuilder.AddKafka("kafkaabstract");
+ var cosmos = AppBuilder.AddAzureCosmosDB("cosmos").UseEmulator();
+
IntegrationServiceABuilder = AppBuilder.AddProject("integrationservicea")
.WithReference(sqlserverContainer)
.WithReference(mysqlContainer)
@@ -79,7 +81,8 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer
.WithReference(rabbitmqAbstract)
.WithReference(mongodbAbstract)
.WithReference(oracleDatabaseAbstract)
- .WithReference(kafkaAbstract);
+ .WithReference(kafkaAbstract)
+ .WithReference(cosmos);
}
}
diff --git a/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj b/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj
index 2e869fe6c6b..b2547a35c41 100644
--- a/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj
+++ b/tests/testproject/TestProject.AppHost/TestProject.AppHost.csproj
@@ -9,6 +9,7 @@
+
diff --git a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs
new file mode 100644
index 00000000000..060fce915a7
--- /dev/null
+++ b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Azure.Cosmos;
+
+public static class CosmosExtensions
+{
+ public static void MapCosmosApi(this WebApplication app)
+ {
+ app.MapGet("/cosmos/verify", VerifyCosmosAsync);
+ }
+
+ private static async Task VerifyCosmosAsync(CosmosClient cosmosClient)
+ {
+ try
+ {
+ var db = (await cosmosClient.CreateDatabaseIfNotExistsAsync("db")).Database;
+ var container = (await db.CreateContainerIfNotExistsAsync("todos", "/id")).Container;
+
+ var id = Guid.NewGuid().ToString();
+ var title = "Do some work.";
+
+ var item = await container.CreateItemAsync(new
+ {
+ id = id,
+ title = title
+ });
+
+ return item.Resource.id == id ? Results.Ok() : Results.Problem();
+ }
+ catch (Exception e)
+ {
+ return Results.Problem(e.ToString());
+ }
+ }
+}
diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs
index 38c8f10e7c0..7f71be0db0f 100644
--- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs
+++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs
@@ -29,6 +29,11 @@
consumerBuilder.Config.AutoOffsetReset = AutoOffsetReset.Earliest;
});
+builder.AddAzureCosmosDB("cosmos", settings =>
+{
+ settings.IgnoreEmulatorCertificate = true;
+});
+
var app = builder.Build();
app.MapHealthChecks("/health");
@@ -53,4 +58,6 @@
app.MapKafkaApi();
+app.MapCosmosApi();
+
app.Run();
diff --git a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj
index 7e72ef932f5..b3351f97b68 100644
--- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj
+++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj
@@ -14,6 +14,7 @@
+