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
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<Description>Azure resource types for .NET Aspire.</Description>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Shared\Cosmos\CosmosConstants.cs" Link="Shared\CosmosConstants.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Expand Down
11 changes: 2 additions & 9 deletions src/Aspire.Hosting.Azure/AzureCosmosDBResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.ObjectModel;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Cosmos;

namespace Aspire.Hosting.Azure.Data.Cosmos;

Expand Down Expand Up @@ -58,13 +59,5 @@ internal void AddDatabase(AzureCosmosDBDatabaseResource database)

file static class AzureCosmosDBEmulatorConnectionString
{
/// <summary>
/// Gets the well-known and documented Azure Cosmos DB emulator account key.
/// See <a href="https://learn.microsoft.com/azure/cosmos-db/emulator#authentication"></a>
/// </summary>
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};";
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

<ItemGroup>
<Compile Include="..\Common\ConfigurationSchemaAttributes.cs" Link="ConfigurationSchemaAttributes.cs" />
<Compile Include="..\..\Shared\Cosmos\CosmosConstants.cs" Link="Shared\CosmosConstants.cs" />
<Compile Include="..\..\Shared\Cosmos\CosmosUtils.cs" Link="Shared\CosmosUtils.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't in

https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-develop-emulator?tabs=windows%2Ccsharp&pivots=api-nosql#connect-to-the-emulator-from-the-sdk

It just has the ServerCertificateCustomValidationCallback and ConnectionMode lines. Why is LimitToEndpoint being set to true here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure of the reason why but without LimitToEndpoint the trick doesn't work. I think it has something to do with the way that the Cosmos client talks to different regional gateways. But I'm not sure how/why that comes into play in the emulator scenario ... but it does seem to make a difference here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the docs be updated then? - cc @Pilchie @sourabh1007

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like document is already updated and LimitToEndpoint is not required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and LimitToEndpoint is not required.

Then why is @mitchdenny saying it is required above?

but without LimitToEndpoint the trick doesn't work.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt docs was update just a day back or so.
@mitchdenny might have referenced it before that change.

Emulator will only have a single region; functional wise it will not impact but unnecessary and confuses.

Ability to ignore SSLCert through connection string is currently in PR stage and next release might ship it. Post that hopefully all above can be updated accordingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mitchdenny - can you verify that we don't need the line setting LimitToEndpoint = true;? I don't want us doing something that isn't in the docs.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoever wrote those docs did not consult with the SDK team. The original version of that doc had it and that might have brought the confusion. We discovered the issue, we corrected the docs (https://github.com/MicrosoftDocs/azure-docs-pr/pull/263406). LimitToEndpoint has nothing to do with emulator or SSL certificates, it's a flag meant to disable cross-region retries on the SDK -> https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/troubleshoot-sdk-availability
image

}

configureClientOptions?.Invoke(clientOptions);

if (serviceKey is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ public sealed class AzureCosmosDBSettings
/// Gets or sets the credential used to authenticate to the Azure Cosmos DB endpoint.
/// </summary>
public TokenCredential? Credential { get; set; }

/// <summary>
/// Controls whether the Cosmos DB emulator certificate is ignored when its use is detected.
/// </summary>
public bool IgnoreEmulatorCertificate { get; set; }
}

Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
19 changes: 19 additions & 0 deletions src/Components/Aspire.Microsoft.Azure.Cosmos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

<ItemGroup>
<Compile Include="..\Common\ConfigurationSchemaAttributes.cs" Link="ConfigurationSchemaAttributes.cs" />
<Compile Include="..\..\Shared\Cosmos\CosmosConstants.cs" Link="Shared\CosmosConstants.cs" />
<Compile Include="..\..\Shared\Cosmos\CosmosUtils.cs" Link="Shared\CosmosUtils.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ public sealed class EntityFrameworkCoreCosmosDBSettings
/// Gets or sets a string value that indicates what Azure region this client will run in.
/// </summary>
public string? Region { get; set; }

/// <summary>
/// Controls whether the Cosmos DB emulator certificate is ignored when its use is detected.
/// </summary>
public bool IgnoreEmulatorCertificate { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,26 @@ The `AddAzureCosmosDB` method will read connection information from the AppHost'
builder.AddCosmosDbContext<MyDbContext>("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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have had the same changes as the other README.


```csharp
// Service code
builder.AddCosmosDbContext<MyDbContext>("cosmos", "mydb", (settings) =>
{
settings.IgnoreEmulatorCertificate = true;
});

```

## Additional documentation

* https://learn.microsoft.com/ef/core/
Expand Down
13 changes: 13 additions & 0 deletions src/Shared/Cosmos/CosmosConstants.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Gets the well-known and documented Azure Cosmos DB emulator account key.
/// See <a href="https://learn.microsoft.com/azure/cosmos-db/emulator#authentication"></a>
/// </summary>
internal const string EmulatorAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
}
22 changes: 22 additions & 0 deletions src/Shared/Cosmos/CosmosUtils.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
40 changes: 40 additions & 0 deletions tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs
Original file line number Diff line number Diff line change
@@ -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<HttpRequestException>()
.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);
});
}
}
5 changes: 4 additions & 1 deletion tests/testproject/TestProject.AppHost/TestProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Projects.IntegrationServiceA>("integrationservicea")
.WithReference(sqlserverContainer)
.WithReference(mysqlContainer)
Expand All @@ -79,7 +81,8 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer
.WithReference(rabbitmqAbstract)
.WithReference(mongodbAbstract)
.WithReference(oracleDatabaseAbstract)
.WithReference(kafkaAbstract);
.WithReference(kafkaAbstract)
.WithReference(cosmos);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
<ProjectReference Include="..\..\..\src\Aspire.Hosting\Aspire.Hosting.csproj" />
<ProjectReference Include="..\TestProject.IntegrationServiceA\TestProject.IntegrationServiceA.csproj" ServiceNameOverride="IntegrationServiceA" />
<ProjectReference Include="..\TestProject.ServiceA\TestProject.ServiceA.csproj" ServiceNameOverride="ServiceA" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IResult> 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());
}
}
}
7 changes: 7 additions & 0 deletions tests/testproject/TestProject.IntegrationServiceA/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
consumerBuilder.Config.AutoOffsetReset = AutoOffsetReset.Earliest;
});

builder.AddAzureCosmosDB("cosmos", settings =>
{
settings.IgnoreEmulatorCertificate = true;
});

var app = builder.Build();

app.MapHealthChecks("/health");
Expand All @@ -53,4 +58,6 @@

app.MapKafkaApi();

app.MapCosmosApi();

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\src\Components\Aspire.Confluent.Kafka\Aspire.Confluent.Kafka.csproj" />
<ProjectReference Include="..\..\..\src\Components\Aspire.Microsoft.Azure.Cosmos\Aspire.Microsoft.Azure.Cosmos.csproj" />
<ProjectReference Include="..\..\..\src\Components\Aspire.Microsoft.Data.SqlClient\Aspire.Microsoft.Data.SqlClient.csproj" />
<ProjectReference Include="..\..\..\src\Components\Aspire.Microsoft.EntityFrameworkCore.SqlServer\Aspire.Microsoft.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\..\src\Components\Aspire.MongoDB.Driver\Aspire.MongoDB.Driver.csproj" />
Expand Down