From be7dc159ede7a2387433791177a74638c6bd73db Mon Sep 17 00:00:00 2001 From: Kevin Pilch Date: Thu, 2 Nov 2023 12:51:31 -0700 Subject: [PATCH] Azure cosmosdb support in aspire (#359) * Initial addition of CosmosDB support, based on SqlServer * Remove Healthchecks support from CosmosDB EF Component * Cleanup connection string handling in Cosmos EF * Cleanup connection string handling in Cosmos component * Update CosmosDB package to get OTel support * Use the parent name for the connection * Udpate manifest strings * Add CosmosDB components to Progress and Telemetry * Rename CosmosDB components to Aspire.Azure.Data.Cosmos[.EntityFrameworkCore] * Rename options -> settings * Rename Cosmos Components to follow naming guidelines * Update to CosmosDB preview package and pin to get OpenTelemetry support * Update comments and add Keyed DI to Aspire.Microsoft.Azure.Cosmos * Add log categories to Cosmos Component schemas * Add basic support for CosmosClientOptions (no IConfiguration binding yet) * Remove healthchecks support from CosmosDB Component * Add README for Aspire.Microsoft.Azure.Cosmos * Add README for Aspire.Microsoft.EntityFrameworkCore.Cosmos, and rename a couple of things * Update config schema to be nested for Aspire.Microsoft.EntityFramework.Cosmos and Aspire.Microsoft.Azure.Cosmos * Rename AzureDataCosmosSettings -> AzureCosmosDBSettings * Update Aspire_Components_Progress.md * Add PackageTags, Descriptions, and Icons * Add AccountEndpoint to ConfigurationScheama.json * Fix DB context builder config * Add xml doc comments for CosmosDB hosting methods and types * Move Cosmos DB hosting to Aspire.Hosting.Azure * Update manifest type names * Respond to PR feedback --- Aspire.sln | 28 ++++ Directory.Packages.props | 4 +- ...smosDBCloudApplicationBuilderExtensions.cs | 60 ++++++++ .../AzureCosmosDBConnectionResource.cs | 21 +++ .../AzureCosmosDatabaseResource.cs | 29 ++++ .../Aspire.Microsoft.Azure.Cosmos.csproj | 20 +++ .../AspireAzureCosmosDBExtensions.cs | 126 ++++++++++++++++ .../AzureCosmosDBSettings.cs | 39 +++++ .../ConfigurationSchema.json | 46 ++++++ .../Aspire.Microsoft.Azure.Cosmos/README.md | 135 ++++++++++++++++++ ...icrosoft.EntityFrameworkCore.Cosmos.csproj | 24 ++++ .../AspireAzureEFCoreCosmosDBExtensions.cs | 132 +++++++++++++++++ .../ConfigurationSchema.json | 80 +++++++++++ .../EntityFrameworkCoreCosmosDBSettings.cs | 54 +++++++ .../README.md | 113 +++++++++++++++ src/Components/Aspire_Components_Progress.md | 4 +- src/Components/Telemetry.md | 30 ++++ src/Shared/AzureCosmosDB_256x.png | Bin 0 -> 20762 bytes ...Aspire.Microsoft.Azure.Cosmos.Tests.csproj | 12 ++ .../ConfigurationTests.cs | 17 +++ .../ConformanceTests.cs | 77 ++++++++++ ...ft.EntityFrameworkCore.Cosmos.Tests.csproj | 17 +++ ...spireAzureEfCoreCosmosDBExtensionsTests.cs | 51 +++++++ .../ConformanceTests_NoPooling.cs | 22 +++ .../ConformanceTests_Pooling.cs | 115 +++++++++++++++ 25 files changed, 1254 insertions(+), 2 deletions(-) create mode 100644 src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs create mode 100644 src/Aspire.Hosting.Azure/AzureCosmosDBConnectionResource.cs create mode 100644 src/Aspire.Hosting.Azure/AzureCosmosDatabaseResource.cs create mode 100644 src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj create mode 100644 src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs create mode 100644 src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs create mode 100644 src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json create mode 100644 src/Components/Aspire.Microsoft.Azure.Cosmos/README.md create mode 100644 src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj create mode 100644 src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs create mode 100644 src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json create mode 100644 src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs create mode 100644 src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md create mode 100644 src/Shared/AzureCosmosDB_256x.png create mode 100644 tests/Aspire.Microsoft.Azure.Cosmos.Tests/Aspire.Microsoft.Azure.Cosmos.Tests.csproj create mode 100644 tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConfigurationTests.cs create mode 100644 tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs create mode 100644 tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests.csproj create mode 100644 tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/AspireAzureEfCoreCosmosDBExtensionsTests.cs create mode 100644 tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_NoPooling.cs create mode 100644 tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_Pooling.cs diff --git a/Aspire.sln b/Aspire.sln index 5647600b1a6..f48ad76f5ab 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -133,6 +133,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Provis EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eShopLite", "eShopLite", "{A68BA1A5-1604-433D-9778-DC0199831C2A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Microsoft.Azure.Cosmos", "src\Components\Aspire.Microsoft.Azure.Cosmos\Aspire.Microsoft.Azure.Cosmos.csproj", "{23298562-C1D4-41CD-83FE-426C94FEE35F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Microsoft.EntityFrameworkCore.Cosmos", "src\Components\Aspire.Microsoft.EntityFrameworkCore.Cosmos\Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj", "{00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Microsoft.Azure.Cosmos.Tests", "tests\Aspire.Microsoft.Azure.Cosmos.Tests\Aspire.Microsoft.Azure.Cosmos.Tests.csproj", "{A5836BC1-6A45-4BB6-9D22-A7F750890AB8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests", "tests\Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests\Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests.csproj", "{FDA02617-9C49-4DA8-A43A-A34DBA9B8596}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogDb", "samples\eShopLite\CatalogDb\CatalogDb.csproj", "{A84C4EE3-2601-4804-BCDC-E9948E164A22}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{991DB378-6CB5-4441-BFC3-657400690FC3}" @@ -370,6 +378,22 @@ Global {D4BD974F-6505-43FC-A94E-2019F0DB5D5D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4BD974F-6505-43FC-A94E-2019F0DB5D5D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4BD974F-6505-43FC-A94E-2019F0DB5D5D}.Release|Any CPU.Build.0 = Release|Any CPU + {23298562-C1D4-41CD-83FE-426C94FEE35F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23298562-C1D4-41CD-83FE-426C94FEE35F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23298562-C1D4-41CD-83FE-426C94FEE35F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23298562-C1D4-41CD-83FE-426C94FEE35F}.Release|Any CPU.Build.0 = Release|Any CPU + {00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63}.Release|Any CPU.Build.0 = Release|Any CPU + {A5836BC1-6A45-4BB6-9D22-A7F750890AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5836BC1-6A45-4BB6-9D22-A7F750890AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5836BC1-6A45-4BB6-9D22-A7F750890AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5836BC1-6A45-4BB6-9D22-A7F750890AB8}.Release|Any CPU.Build.0 = Release|Any CPU + {FDA02617-9C49-4DA8-A43A-A34DBA9B8596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDA02617-9C49-4DA8-A43A-A34DBA9B8596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDA02617-9C49-4DA8-A43A-A34DBA9B8596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDA02617-9C49-4DA8-A43A-A34DBA9B8596}.Release|Any CPU.Build.0 = Release|Any CPU {A84C4EE3-2601-4804-BCDC-E9948E164A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A84C4EE3-2601-4804-BCDC-E9948E164A22}.Debug|Any CPU.Build.0 = Debug|Any CPU {A84C4EE3-2601-4804-BCDC-E9948E164A22}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -446,6 +470,10 @@ Global {E2EC79D0-80F7-4471-9613-D7C8C3D52F95} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {D4BD974F-6505-43FC-A94E-2019F0DB5D5D} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {A68BA1A5-1604-433D-9778-DC0199831C2A} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} + {23298562-C1D4-41CD-83FE-426C94FEE35F} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} + {00C9BA50-2AFB-4D9C-A2D6-8154BCCD0A63} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} + {A5836BC1-6A45-4BB6-9D22-A7F750890AB8} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} + {FDA02617-9C49-4DA8-A43A-A34DBA9B8596} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {A84C4EE3-2601-4804-BCDC-E9948E164A22} = {A68BA1A5-1604-433D-9778-DC0199831C2A} {4D8A92AB-4E77-4965-AD8E-8E206DCE66A4} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {165411FE-755E-4869-A756-F87F455860AC} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} diff --git a/Directory.Packages.props b/Directory.Packages.props index 59dad36f960..a5b99920fbd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + @@ -44,6 +45,7 @@ + @@ -100,4 +102,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs new file mode 100644 index 00000000000..3d307a2ff78 --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBCloudApplicationBuilderExtensions.cs @@ -0,0 +1,60 @@ +// 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.ApplicationModel; +using Aspire.Hosting.Azure.Data.Cosmos; +using System.Text.Json; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Azure Cosmos DB resources to an . +/// +public static class AzureCosmosDBCloudApplicationBuilderExtensions +{ + /// + /// Adds an Azure Cosmos DB connection to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The connection string. + /// A reference to the . + public static IResourceBuilder AddAzureCosmosDB( + this IDistributedApplicationBuilder builder, + string name, + string? connectionString = null) + { + var connection = new AzureCosmosDBConnectionResource(name, connectionString); + return builder.AddResource(connection) + .WithAnnotation(new ManifestPublishingCallbackAnnotation(jsonWriter => WriteCosmosDBConnectionToManifest(jsonWriter, connection))); + } + + private static void WriteCosmosDBConnectionToManifest(Utf8JsonWriter jsonWriter, AzureCosmosDBConnectionResource cosmosDbConnection) + { + jsonWriter.WriteString("type", "azure.cosmosdb.connection.v0"); + jsonWriter.WriteString("connectionString", cosmosDbConnection.GetConnectionString()); + } + + private static void WriteCosmosDBDatabaseToManifest(Utf8JsonWriter jsonWriter, AzureCosmosDatabaseResource cosmosDatabase) + { + jsonWriter.WriteString("type", "azure.cosmosdb.database.v0"); + jsonWriter.WriteString("parent", cosmosDatabase.Parent.Name); + jsonWriter.WriteString("databaseName", cosmosDatabase.Name); + } + + /// + /// Adds an Azure Cosmos DB database to a . + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// A reference to the . + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) + { + var cosmosDatabase = new AzureCosmosDatabaseResource(name, builder.Resource); + return builder + .ApplicationBuilder + .AddResource(cosmosDatabase) + .WithAnnotation(new ManifestPublishingCallbackAnnotation( + (json) => WriteCosmosDBDatabaseToManifest(json, cosmosDatabase))); + } +} diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDBConnectionResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDBConnectionResource.cs new file mode 100644 index 00000000000..53b1f2381e6 --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureCosmosDBConnectionResource.cs @@ -0,0 +1,21 @@ +// 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.ApplicationModel; + +namespace Aspire.Hosting.Azure.Data.Cosmos; + +/// +/// Represents a connection to an Azure Cosmos DB account. +/// +/// The resource name. +/// The connection string to use to connect. +public class AzureCosmosDBConnectionResource(string name, string? connectionString) + : Resource(name), IResourceWithConnectionString +{ + /// + /// Gets the connection string to use for this database. + /// + /// The connection string to use for this database. + public string? GetConnectionString() => connectionString; +} diff --git a/src/Aspire.Hosting.Azure/AzureCosmosDatabaseResource.cs b/src/Aspire.Hosting.Azure/AzureCosmosDatabaseResource.cs new file mode 100644 index 00000000000..96cc0feb4b2 --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureCosmosDatabaseResource.cs @@ -0,0 +1,29 @@ +// 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.ApplicationModel; + +namespace Aspire.Hosting.Azure.Data.Cosmos; + +/// +/// Represents an Azure Cosmos DB database. +/// +/// The database name. +/// The parent . +public class AzureCosmosDatabaseResource(string name, AzureCosmosDBConnectionResource parent) + : Resource(name), IResourceWithParent, IResourceWithConnectionString +{ + /// + /// Gets the parent . + /// + public AzureCosmosDBConnectionResource Parent { get; } = parent; + + /// + /// Gets the connection string to use for this database. + /// + /// The connection string to use for this database. + public string? GetConnectionString() + { + return Parent.GetConnectionString(); + } +} 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 new file mode 100644 index 00000000000..e41dd949483 --- /dev/null +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/Aspire.Microsoft.Azure.Cosmos.csproj @@ -0,0 +1,20 @@ + + + + $(NetCurrent) + true + false + false + $(ComponentAzurePackageTags) cosmos cosmosdb data database db + A client for Azure Cosmos DB that integrates with Aspire, including logging and telemetry. + $(SharedDir)AzureCosmosDB_256x.png + + + + + + + + + + diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs new file mode 100644 index 00000000000..cfe43cbc4e0 --- /dev/null +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AspireAzureCosmosDBExtensions.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Microsoft.Azure.Cosmos; +using Azure.Identity; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Azure CosmosDB extension +/// +public static class AspireAzureCosmosDBExtensions +{ + private const string DefaultConfigSectionName = "Aspire:Microsoft:Azure:Cosmos"; + + /// + /// Registers as a singleton in the services provided by the . + /// Configures logging and telemetry for the . + /// + /// The to read config from and add services to. + /// The connection name to use to find a connection string. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section. + /// If required ConnectionString is not provided in configuration section + public static void AddAzureCosmosDB( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action? configureClientOptions = null) + { + AddAzureCosmosDB(builder, DefaultConfigSectionName, configureSettings, configureClientOptions, connectionName, serviceKey: null); + } + + /// + /// Registers as a singleton for given in the services provided by the . + /// Configures logging and telemetry for the . + /// + /// The to read config from and add services to. + /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration. + /// An optional method that can be used for customizing the . + /// Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section. + /// If required ConnectionString is not provided in configuration section + public static void AddKeyedAzureCosmosDB( + this IHostApplicationBuilder builder, + string name, + Action? configureSettings = null, + Action? configureClientOptions = null) + { + AddAzureCosmosDB(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, configureClientOptions, connectionName: name, serviceKey: name); + } + + private static void AddAzureCosmosDB( + this IHostApplicationBuilder builder, + string configurationSectionName, + Action? configureSettings, + Action? configureClientOptions, + string connectionName, + string? serviceKey) + { + ArgumentNullException.ThrowIfNull(builder); + + var settings = new AzureCosmosDBSettings(); + builder.Configuration.GetSection(configurationSectionName).Bind(settings); + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + settings.AccountEndpoint = uri; + } + else + { + settings.ConnectionString = connectionString; + } + } + + configureSettings?.Invoke(settings); + + var clientOptions = new CosmosClientOptions(); + // Needs to be enabled for either logging or tracing to work. + clientOptions.CosmosClientTelemetryOptions.DisableDistributedTracing = false; + if (settings.Tracing) + { + builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder => + { + tracerProviderBuilder.AddSource("Azure.Cosmos.Operation"); + }); + } + + configureClientOptions?.Invoke(clientOptions); + + if (serviceKey is null) + { + builder.Services.AddSingleton(_ => ConfigureDb()); + } + else + { + builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureDb()); + } + + CosmosClient ConfigureDb() + { + if (!string.IsNullOrEmpty(settings.ConnectionString)) + { + return new CosmosClient(settings.ConnectionString, clientOptions); + } + else if (settings.AccountEndpoint is not null) + { + var credential = settings.Credential ?? new DefaultAzureCredential(); + return new CosmosClient(settings.AccountEndpoint.OriginalString, credential, clientOptions); + } + else + { + throw new InvalidOperationException( + $"A CosmosClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " + + $"{nameof(settings.ConnectionString)} or {nameof(settings.AccountEndpoint)} must be provided " + + $"in the '{configurationSectionName}' configuration section."); + } + } + } +} diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs b/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs new file mode 100644 index 00000000000..98b9aaae607 --- /dev/null +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/AzureCosmosDBSettings.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; + +namespace Aspire.Microsoft.Azure.Cosmos; + +/// +/// The settings relevant to accessing Azure Cosmos DB. +/// +public sealed class AzureCosmosDBSettings +{ + /// + /// Gets or sets the connection string of the Azure Cosmos database to connect to. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not. + /// Enabled by default. + /// + public bool Tracing { get; set; } = true; + + /// + /// A referencing the Azure Cosmos DB Endpoint. + /// This is likely to be similar to "https://{account_name}.queue.core.windows.net". + /// + /// + /// Must not contain shared access signature. + /// Used along with to establish the connection. + /// + public Uri? AccountEndpoint { get; set; } + + /// + /// Gets or sets the credential used to authenticate to the Azure Cosmos DB endpoint. + /// + public TokenCredential? Credential { get; set; } +} + diff --git a/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json new file mode 100644 index 00000000000..9ba4f921b4a --- /dev/null +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json @@ -0,0 +1,46 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Azure-Cosmos-Operation-Request-Diagnostics": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, + "properties": { + "Aspire": { + "type": "object", + "properties": { + "Microsoft": { + "type": "object", + "properties": { + "Azure": { + "type": "object", + "properties": { + "Cosmos": { + "type": "object", + "properties": { + "ConnectionString": { + "type": "string", + "description": "Gets or sets the connection string of the Azure Cosmos DB to connect to. If both are provided, 'ConnectionString' takes precedence over 'AccountEndpoint'." + }, + "AccountEndpoint": { + "type": "string", + "format": "uri", + "description": "Gets or sets the account endpoint of the Azure Cosmos DB to connect to. If both are provided, 'ConnectionString' takes precedence over 'AccountEndpoint'." + }, + "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 new file mode 100644 index 00000000000..2aeefdbafce --- /dev/null +++ b/src/Components/Aspire.Microsoft.Azure.Cosmos/README.md @@ -0,0 +1,135 @@ +# Aspire.Microsoft.Azure.Cosmos library + +Registers [CosmosClient](https://learn.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclient) as a singleton in the DI container for connecting to Azure Cosmos DB. Enables corresponding logging and telemetry. + +## Getting started + +### Prerequisites + +- Azure subscription - [create one for free](https://azure.microsoft.com/free/) +- Azure Cosmos DB account - [create a Cosmos DB account](https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-create-account) + +### Install the package + +Install the Aspire Microsft Azure Cosmos DB library with [NuGet][nuget]: + +```dotnetcli +dotnet add package Aspire.Microsoft.Azure.Cosmos +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddAzureCosmosDB` extension method to register a `CosmosClient` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddAzureCosmosDB("cosmosConnectionName"); +``` + +You can then retrieve the `CosmosClient` instance using dependency injection. For example, to retrieve the client from a Web API controller: + +```csharp +private readonly CosmosClient _client; + +public ProductsController(CosmosClient client) +{ + _client = client; +} +``` + +See the [Azure Cosmos DB documentation](https://learn.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclient) for examples on using the `CosmosClient`. + +## Configuration + +The Aspire Azure Cosmos DB library provides multiple options to configure the Azure Cosmos DB connection based on the requirements and conventions of your project. Note that either an `AccountEndpoint` or a `ConnectionString` is a required to be supplied. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddAzureCosmosDB()`: + +```csharp +builder.AddAzureCosmosDB("cosmosConnectionName"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported: + +#### Account Endpoint + +The recommended approach is to use an AccountEndpoint, which works with the `AzureCosmosDBSettings.Credential` property to establish a connection. If no credential is configured, the [DefaultAzureCredential](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential) is used. + +```json +{ + "ConnectionStrings": { + "cosmosConnectionName": "https://{account_name}.documents.azure.com:443/" + } +} +``` + +#### Connection string + +Alternatively, an [Azure Cosmos DB connection string](https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-dotnet-get-started#connect-with-a-connection-string) can be used. + +```json +{ + "ConnectionStrings": { + "cosmosConnectionName": "AccountEndpoint=https://{account_name}.documents.azure.com:443/;AccountKey={account_key};" + } +} +``` + +### Use configuration providers + +The Aspire Microsoft Azure Cosmos DB library supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `AzureCosmosDBSettings` and `QueueClientOptions` from configuration by using the `Aspire:Microsoft:Azure:Cosmos` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Microsoft": { + "Azure": { + "Cosmos": { + "Tracing": true, + } + } + } + } +} +``` + +### Use inline delegates + +You can also pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable tracing from code: + +```csharp + builder.AddAzureCosmosDB("cosmosConnectionName", settings => settings.Tracing = false); +``` + +You can also setup the [CosmosClientOptions](https://learn.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclientoptions) using the optional `Action configureClientOptions` parameter of the `AddAzureCosmosDB` method. For example, to set the `ApplicationName` "User-Agent" header suffix for all requests issues by this client: + +```csharp + builder.AddAzureCosmosDB("cosmosConnectionName", configureClientOptions: clientOptions => clientOptions.ApplicationName = "myapp"); +``` + +## AppHost extensions + +In your AppHost project, add a Cosmos DB connection and consume the connection using the following methods: + +```csharp +var cosmosdb = builder.AddAzureCosmosDB("cdb").AddDatabase("cosmosdb"); + +var myService = builder.AddProject() + .WithReference(cosmosdb); +``` + +The `AddAzureCosmosDB` method will read connection information from the AppHost's configuration (for example, from "user secrets") under the `ConnectionStrings:cosmosdb` config key. The `WithReference` method passes that connection information into a connection string named `cosmosdb` in the `MyService` project. In the _Program.cs_ file of `MyService`, the connection can be consumed using: + +```csharp +builder.AddAzureCosmosDB("cosmosdb"); +``` + +## Additional documentation + +* https://learn.microsoft.com/azure/cosmos-db/nosql/sdk-dotnet-v3 +* https://github.com/dotnet/aspire/tree/main/src/Components/README.md + +## Feedback & contributing + +https://github.com/dotnet/aspire 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 new file mode 100644 index 00000000000..913d7e60273 --- /dev/null +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/Aspire.Microsoft.EntityFrameworkCore.Cosmos.csproj @@ -0,0 +1,24 @@ + + + + $(NetCurrent) + true + false + false + $(ComponentEfCorePackageTags) azure cosmos cosmosdb + A Microsoft Azure Cosmos DB provider for Entity Framework Core that integrates with Aspire, including connection pooling, logging, and telemetry. + $(SharedDir)AzureCosmosDB_256x.png + + + + + + + + + + + + + + diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs new file mode 100644 index 00000000000..fa518a0a41f --- /dev/null +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/AspireAzureEFCoreCosmosDBExtensions.cs @@ -0,0 +1,132 @@ +// 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 Aspire.Microsoft.EntityFrameworkCore.Cosmos; +using Azure.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for configuring EntityFrameworkCore DbContext to Azure Cosmos DB +/// +public static class AspireAzureEFCoreCosmosDBExtensions +{ + private const string DefaultConfigSectionName = "Aspire:Microsoft:EntityFrameworkCore:Cosmos"; + private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; + + /// + /// Registers the given as a service in the services provided by the . + /// Configures the connection pooling, logging and telemetry for the . + /// + /// The that needs to be registered. + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// The name of the database to use within the Azure Cosmos DB account. + /// An optional delegate that can be used for customizing settings. It's invoked after the settings are read from the configuration. + /// An optional delegate to configure the for the context. + /// Thrown if mandatory is null. + /// Thrown when mandatory is not provided. + public static void AddCosmosDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( + this IHostApplicationBuilder builder, + string connectionName, + string databaseName, + Action? configureSettings = null, + Action? configureDbContextOptions = null) where TContext : DbContext + { + ArgumentNullException.ThrowIfNull(builder); + + var settings = new EntityFrameworkCoreCosmosDBSettings(); + var typeSpecificSectionName = $"{DefaultConfigSectionName}:{typeof(TContext).Name}"; + var typeSpecificConfigurationSection = builder.Configuration.GetSection(typeSpecificSectionName); + if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380 + { + typeSpecificConfigurationSection.Bind(settings); + } + else + { + builder.Configuration.GetSection(DefaultConfigSectionName).Bind(settings); + } + + if (builder.Configuration.GetConnectionString(connectionName) is string connectionString) + { + if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri)) + { + settings.AccountEndpoint = uri; + } + else + { + settings.ConnectionString = connectionString; + } + } + configureSettings?.Invoke(settings); + + if (settings.DbContextPooling) + { + builder.Services.AddDbContextPool(ConfigureDbContext); + } + else + { + builder.Services.AddDbContext(ConfigureDbContext); + } + + if (settings.Tracing) + { + builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder => + { + tracerProviderBuilder.AddEntityFrameworkCoreInstrumentation(); + tracerProviderBuilder.AddSource("Azure.Cosmos.Operation"); + }); + } + + if (settings.Metrics) + { + builder.Services.AddOpenTelemetry().WithMetrics(meterProviderBuilder => + { + meterProviderBuilder.AddEventCountersInstrumentation(eventCountersInstrumentationOptions => + { + // https://github.com/dotnet/efcore/blob/main/src/EFCore/Infrastructure/EntityFrameworkEventSource.cs#L45 + eventCountersInstrumentationOptions.AddEventSources("Microsoft.EntityFrameworkCore"); + }); + }); + } + + void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder) + { + if (!string.IsNullOrEmpty(settings.ConnectionString)) + { + dbContextOptionsBuilder.UseCosmos(settings.ConnectionString, databaseName, UseCosmosBody); + } + else if (settings.AccountEndpoint is not null) + { + var credential = settings.Credential ?? new DefaultAzureCredential(); + dbContextOptionsBuilder.UseCosmos(settings.AccountEndpoint.OriginalString, credential, databaseName, UseCosmosBody); + } + else + { + throw new InvalidOperationException( + $"A DbContext could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " + + $"{nameof(settings.ConnectionString)} or {nameof(settings.AccountEndpoint)} must be provided " + + $"in the '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section."); + } + + configureDbContextOptions?.Invoke(dbContextOptionsBuilder); + } + + void UseCosmosBody(CosmosDbContextOptionsBuilder builder) + { + // We don't register logger factory, because there is no need to: + // https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.useloggerfactory?view=efcore-7.0#remarks + if (settings.Region is not null) + { + builder.Region(settings.Region); + } + } + } +} diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json new file mode 100644 index 00000000000..40dc2ae5b25 --- /dev/null +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json @@ -0,0 +1,80 @@ +{ + "definitions": { + "logLevel": { + "properties": { + "Azure-Cosmos-Operation-Request-Diagnostics": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore.ChangeTracking": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore.Database": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore.Database.Command": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore.Infrastructure": { + "$ref": "#/definitions/logLevelThreshold" + }, + "Microsoft.EntityFrameworkCore.Query": { + "$ref": "#/definitions/logLevelThreshold" + } + } + } + }, + "properties": { + "Aspire": { + "type": "object", + "properties": { + "Microsoft": { + "type": "object", + "properties": { + "EntityFrameworkCore": { + "type": "object", + "properties": { + "Cosmos": { + "type": "object", + "properties": { + "AccountEndpoint": { + "type": "string", + "format": "uri", + "description": "Gets or sets the account endpoint of the Azure Cosmos DB account to connect to. Used along with \"Credential\" to establish the connection." + }, + "ConnectionString": { + "type": "string", + "description": "Gets or sets the connection string of the Azure Cosmos DB account to connect to." + }, + "DbContextPooling": { + "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.", + "default": true + }, + "Tracing": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.", + "default": true + }, + "Metrics": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.", + "default": true + }, + "Region": { + "type": "string", + "description": "Gets or sets a string value that indicates what Azure region this client will run in." + } + } + } + } + } + } + } + } + } + }, + "type": "object" +} diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs new file mode 100644 index 00000000000..320f595ccfb --- /dev/null +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/EntityFrameworkCoreCosmosDBSettings.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Core; + +namespace Aspire.Microsoft.EntityFrameworkCore.Cosmos; + +/// +/// The settings relevant to accessing Azure Cosmos DB database using EntityFrameworkCore. +/// +public sealed class EntityFrameworkCoreCosmosDBSettings +{ + /// + /// The connection string of the Azure Cosmos DB server database to connect to. + /// + public string? ConnectionString { get; set; } + + /// + /// A referencing the Azure Cosmos DB Endpoint. + /// This is likely to be similar to "https://{account_name}.queue.core.windows.net". + /// + /// + /// Must not contain shared access signature. + /// Used along with to establish the connection. + /// + public Uri? AccountEndpoint { get; set; } + + /// + /// Gets or sets the credential used to authenticate to the Azure Cosmos DB endpoint. + /// + public TokenCredential? Credential { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it's requested. + /// + public bool DbContextPooling { get; set; } = true; + + /// + /// Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not. + /// Enabled by default. + /// + public bool Tracing { get; set; } = true; + + /// + /// Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not. + /// Enabled by default. + /// + public bool Metrics { get; set; } = true; + + /// + /// Gets or sets a string value that indicates what Azure region this client will run in. + /// + public string? Region { get; set; } +} diff --git a/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md new file mode 100644 index 00000000000..f1d90dbaad0 --- /dev/null +++ b/src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/README.md @@ -0,0 +1,113 @@ +# Aspire.Microsoft.EntityFrameworkCore.Cosmos library + +Registers [EntityFrameworkCore](https://learn.microsoft.com/en-us/ef/core/) [DbContext](https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontext) in the DI container for connecting to Azure Cosmos DB. Enables connection pooling, logging and telemetry. + +## Getting started + +### Prerequisites + +- CosmosDB database and connection string for accessing the database. + +### Install the package + +Install the Aspire Microsoft EntityFrameworkCore Cosmos library with [NuGet][nuget]: + +```dotnetcli +dotnet add package Aspire.Microsoft.EntityFrameworkCore.Cosmos +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddCosmosDbContext` extension method to register a `DbContext` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddCosmosDbContext("cosmosdb"); +``` + +You can then retrieve the `MyDbContext` instance using dependency injection. For example, to retrieve the context from a Web API controller: + +```csharp +private readonly MyDbContext _context; + +public ProductsController(MyDbContext context) +{ + _context = context; +} +``` + +## Configuration + +The Aspire Microsoft EntityFrameworkCore Cosmos component provides multiple options to configure the database connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddCosmosDbContext()`: + +```csharp +builder.AddCosmosDbContext("myConnection"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "myConnection": "AccountEndpoint=https://{account_name}.documents.azure.com:443/;AccountKey={account_key};" + } +} +``` + +See the [ConnectionString documentation](https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-dotnet-get-started#connect-with-a-connection-string) for more information. + +### Use configuration providers + +The Aspire Microsoft EntityFrameworkCore Cosmos component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `EntityFrameworkCoreCosmosDBSettings` from configuration by using the `Aspire:Microsaoft:EntityFrameworkCore:Cosmos` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Microsoft": { + "EntityFrameworkCore": { + "Cosmos": { + "DbContextPooling": true, + "Tracing": false + } + } + } + } +} +``` + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable tracing from code: + +```csharp + builder.AddCosmosDbContext("cosmosdb", settings => settings.Tracing = false); +``` + +## AppHost extensions + +In your AppHost project, add a Cosmos DB connection and consume the connection using the following methods:: + +```csharp +var cosmosdb = builder.AddAzureCosmosDB("cdb").AddDatabase("cosmosdb"); + +var myService = builder.AddProject() + .WithReference(cosmosdb); +``` + +The `WithReference` method configures a connection in the `MyService` project named `cosmosdb`. In the _Program.cs_ file of `MyService`, the database connection can be consumed using: + +```csharp +builder.AddCosmosDbContext("cosmosdb"); +``` + +## Additional documentation + +* https://learn.microsoft.com/ef/core/ +* https://github.com/dotnet/aspire/tree/main/src/Components/README.md + +## Feedback & contributing + +https://github.com/dotnet/aspire \ No newline at end of file diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 20dbc4a81b0..38c7fc20aaa 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -6,9 +6,11 @@ As part of the Aspire November preview, we want to include a set of Aspire Compo | --------------------------------------- | :---------------------------------: | :-----------------------: | :----------------------------------------------------: | :-------------------------: | :-----------------: | :-----------------: | :-----------------: | :-----------------------------: | | Npgsql | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Npgsql.EntityFrameworkCore.PostgreSQL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Microsoft.Azure.Cosmos | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | | Microsoft.Data.SqlClient | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | +| Microsoft.EntityFramework.Cosmos | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | | Microsoft.EntityFrameworkCore.SqlServer | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Aspire.Azure.Data.Tables | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | +| Azure.Data.Tables | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.Messaging.ServiceBus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.Security.KeyVault | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Azure.Storage.Blobs | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index d3bd8a82194..715f8d8a9f4 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -47,6 +47,13 @@ Aspire.Azure.Storage.Queues: - Metric names: - none (currently not supported by the Azure SDK) +Aspire.Microsoft.Azure.Cosmos: +- Log categories: + - "Azure-Cosmos-Operation-Request-Diagnostics" +- Activity source names: + - "Azure.Cosmos.Operation" +- Metric names: + Aspire.Microsoft.Data.SqlClient: - Log categories: - none (the client does not provide an easy way to integrate it with logger factory) @@ -71,6 +78,29 @@ Aspire.Microsoft.Data.SqlClient: - "number-of-stasis-connections" - "number-of-reclaimed-connections" +Aspire.Microsoft.EntityFrameworkCore.Cosmos: +- Log categories: + - "Azure-Cosmos-Operation-Request-Diagnostics" + - "Microsoft.EntityFrameworkCore.ChangeTracking", + - "Microsoft.EntityFrameworkCore.Database.Command", + - "Microsoft.EntityFrameworkCore.Infrastructure", + - "Microsoft.EntityFrameworkCore.Query", +- Activity source names: + - "Azure.Cosmos.Operation" + - "OpenTelemetry.Instrumentation.EntityFrameworkCore" +- Metric names: + - "Microsoft.EntityFrameworkCore": + - "ec_Microsoft_EntityFrameworkCore_active_db_contexts" + - "ec_Microsoft_EntityFrameworkCore_total_queries" + - "ec_Microsoft_EntityFrameworkCore_queries_per_second" + - "ec_Microsoft_EntityFrameworkCore_total_save_changes" + - "ec_Microsoft_EntityFrameworkCore_save_changes_per_second" + - "ec_Microsoft_EntityFrameworkCore_compiled_query_cache_hit_rate" + - "ec_Microsoft_Entity_total_execution_strategy_operation_failures" + - "ec_Microsoft_E_execution_strategy_operation_failures_per_second" + - "ec_Microsoft_EntityFramew_total_optimistic_concurrency_failures" + - "ec_Microsoft_EntityF_optimistic_concurrency_failures_per_second" + Aspire.Microsoft.EntityFrameworkCore.SqlServer: - Log categories: - "Microsoft.EntityFrameworkCore.ChangeTracking" diff --git a/src/Shared/AzureCosmosDB_256x.png b/src/Shared/AzureCosmosDB_256x.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2850532c66c8eba547f3509ecf14607df2b593 GIT binary patch literal 20762 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 literal 0 HcmV?d00001 diff --git a/tests/Aspire.Microsoft.Azure.Cosmos.Tests/Aspire.Microsoft.Azure.Cosmos.Tests.csproj b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/Aspire.Microsoft.Azure.Cosmos.Tests.csproj new file mode 100644 index 00000000000..07a7d23049b --- /dev/null +++ b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/Aspire.Microsoft.Azure.Cosmos.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(NetCurrent) + + + + + + + + diff --git a/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConfigurationTests.cs b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConfigurationTests.cs new file mode 100644 index 00000000000..df3284ebf7f --- /dev/null +++ b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConfigurationTests.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Aspire.Microsoft.Azure.Cosmos.Tests; + +public class ConfigurationTests +{ + [Fact] + public void ConnectionStringIsNullByDefault() + => Assert.Null(new AzureCosmosDBSettings().ConnectionString); + + [Fact] + public void TracingIsEnabledByDefault() + => Assert.True(new AzureCosmosDBSettings().Tracing); +} diff --git a/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs new file mode 100644 index 00000000000..4ff5fa82f30 --- /dev/null +++ b/tests/Aspire.Microsoft.Azure.Cosmos.Tests/ConformanceTests.cs @@ -0,0 +1,77 @@ +// 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.ConformanceTests; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Aspire.Microsoft.Azure.Cosmos.Tests; + +public class ConformanceTests : ConformanceTests +{ + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + protected override string ActivitySourceName => "Azure.Cosmos.Operation"; + + protected override string[] RequiredLogCategories => Array.Empty(); + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[1] + { + new KeyValuePair(CreateConfigKey("Aspire:Microsoft:Azure:Cosmos", key, "ConnectionString"), + "AccountEndpoint=https://example.documents.azure.com:443/;AccountKey=fake;") + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + if (key is null) + { + builder.AddAzureCosmosDB("cosmosdb", configure); + } + else + { + builder.AddKeyedAzureCosmosDB(key, configure); + } + } + + protected override void SetHealthCheck(AzureCosmosDBSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetTracing(AzureCosmosDBSettings options, bool enabled) + => options.Tracing = enabled; + + protected override void SetMetrics(AzureCosmosDBSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override string JsonSchemaPath + => "src/Components/Aspire.Microsoft.Azure.Cosmos/ConfigurationSchema.json"; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Microsoft": { + "Azure": { + "Cosmos": { + "ConnectionString": "YOUR_CONNECTION_STRING", + "Tracing": true + } + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Microsoft":{ "Azure": { "Cosmos": { "AccountEndpoint": 3 }}}}}""", "Value is \"integer\" but should be \"string\""), + ("""{"Aspire": { "Microsoft":{ "Azure": { "Cosmos": { "AccountEndpoint": "hello" }}}}}""", "Value does not match format \"uri\"") + }; + + protected override void TriggerActivity(CosmosClient service) + { + // TODO: Get rid of GetAwaiter().GetResult() + service.ReadAccountAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests.csproj b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests.csproj new file mode 100644 index 00000000000..6ef388b18b7 --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests.csproj @@ -0,0 +1,17 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/AspireAzureEfCoreCosmosDBExtensionsTests.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/AspireAzureEfCoreCosmosDBExtensionsTests.cs new file mode 100644 index 00000000000..d462195c43f --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/AspireAzureEfCoreCosmosDBExtensionsTests.cs @@ -0,0 +1,51 @@ +// 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 Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests; + +public class AspireAzureEfCoreCosmosDBExtensionsTests +{ + private const string ConnectionString = "AccountEndpoint=https://fake-account.documents.azure.com:443/;AccountKey=;"; + + [Fact] + public void CanConfigureDbContextOptions() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:cosmosConnection", ConnectionString), + new KeyValuePair("Aspire:Microsoft:EntityFrameworkCore:Cosmos:Region", "westus"), + ]); + + builder.AddCosmosDbContext("cosmosConnection", "databaseName", configureDbContextOptions: optionsBuilder => + { + optionsBuilder.UseCosmos(ConnectionString, "databaseName", cosmosBuilder => + { + cosmosBuilder.RequestTimeout(TimeSpan.FromSeconds(608)); + }); + }); + + var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // Ensure the RequestTimeout from config size was respected + Assert.Equal(TimeSpan.FromSeconds(608), extension.RequestTimeout); + + // Ensure the Region from the lambda was respected + Assert.Equal("westus", extension.Region); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } +} diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_NoPooling.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_NoPooling.cs new file mode 100644 index 00000000000..374c8117a20 --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_NoPooling.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 Aspire.Components.Common.Tests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests; + +public class ConformanceTests_NoPooling : ConformanceTests_Pooling +{ + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Scoped; + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + builder.AddCosmosDbContext("cosmosdb", "TestDatabase", settings => + { + settings.DbContextPooling = false; + configure?.Invoke(settings); + }); + } +} diff --git a/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_Pooling.cs b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_Pooling.cs new file mode 100644 index 00000000000..1ac9540ad66 --- /dev/null +++ b/tests/Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests/ConformanceTests_Pooling.cs @@ -0,0 +1,115 @@ +// 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.Components.ConformanceTests; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Microsoft.EntityFrameworkCore.Cosmos.Tests; + +public class ConformanceTests_Pooling : ConformanceTests +{ + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/cb5b2193ef9cacc0b9ef699e085022577551bf85/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs#L38 + protected override string ActivitySourceName => "OpenTelemetry.Instrumentation.EntityFrameworkCore"; + + protected override string[] RequiredLogCategories => new string[] + { + "Microsoft.EntityFrameworkCore.ChangeTracking", + "Microsoft.EntityFrameworkCore.Database.Command", + "Microsoft.EntityFrameworkCore.Infrastructure", + "Microsoft.EntityFrameworkCore.Query", + }; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[] + { + new KeyValuePair("Aspire:Microsoft:EntityFrameworkCore:Cosmos:ConnectionString", + "Host=fake;Database=catalog"), + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + => builder.AddCosmosDbContext("cosmosdb", "TestDatabase", configure); + + protected override void SetHealthCheck(EntityFrameworkCoreCosmosDBSettings options, bool enabled) + => throw new NotImplementedException(); + + protected override void SetTracing(EntityFrameworkCoreCosmosDBSettings options, bool enabled) + => options.Tracing = enabled; + + protected override void SetMetrics(EntityFrameworkCoreCosmosDBSettings options, bool enabled) + => options.Metrics = enabled; + + protected override string JsonSchemaPath + => "src/Components/Aspire.Microsoft.EntityFrameworkCore.Cosmos/ConfigurationSchema.json"; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Microsoft": { + "EntityFrameworkCore": { + "Cosmos": { + "ConnectionString": "YOUR_CONNECTION_STRING", + "Tracing": true, + "Metrics": true + } + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Microsoft":{ "EntityFrameworkCore": { "Cosmos": { "AccountEndpoint": 3 }}}}}""", "Value is \"integer\" but should be \"string\""), + ("""{"Aspire": { "Microsoft":{ "EntityFrameworkCore": { "Cosmos": { "AccountEndpoint": "hello" }}}}}""", "Value does not match format \"uri\""), + ("""{"Aspire": { "Microsoft":{ "EntityFrameworkCore": { "Cosmos": { "Region": 3 }}}}}""", "Value is \"integer\" but should be \"string\""), + }; + + protected override void TriggerActivity(TestDbContext service) + { + if (service.Database.CanConnect()) + { + service.Database.EnsureCreated(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Required to verify pooling without touching DB")] + public void DbContextPoolingRegistersIDbContextPool(bool enabled) + { + using IHost host = CreateHostWithComponent(options => options.DbContextPooling = enabled); + + IDbContextPool? pool = host.Services.GetService>(); + + Assert.Equal(enabled, pool is not null); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DbContextCanBeAlwaysResolved(bool enabled) + { + using IHost host = CreateHostWithComponent(options => options.DbContextPooling = enabled); + + TestDbContext? dbContext = host.Services.GetService(); + + Assert.NotNull(dbContext); + } + + [ConditionalFact] + public void TracingEnablesTheRightActivitySource() + { + SkipIfCanNotConnectToServer(); + + RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + } +}