Skip to content

Commit

Permalink
Azure cosmosdb support in aspire (#359) (#669)
Browse files Browse the repository at this point in the history
* 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

Co-authored-by: Kevin Pilch <me@pilchie.com>
  • Loading branch information
eerhardt and Pilchie authored Nov 2, 2023
1 parent 2813800 commit ac8c560
Show file tree
Hide file tree
Showing 25 changed files with 1,254 additions and 2 deletions.
28 changes: 28 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageVersion Include="Azure.Security.KeyVault.Secrets" Version="4.5.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.18.0" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.16.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.36.0-preview" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.7.1" />
<!-- Azure Management SDK for .NET dependencies -->
<PackageVersion Include="Azure.ResourceManager.KeyVault" Version="1.2.0-beta.2" />
Expand Down Expand Up @@ -44,6 +45,7 @@
<!-- sql client dependencies -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.1.1" />
<!-- efcore dependencies -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="$(EfCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EfCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(EfCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="$(EfCoreVersion)" />
Expand Down Expand Up @@ -100,4 +102,4 @@
<PackageVersion Include="Microsoft.Signed.Wix" Version="1.0.0-v3.14.0.5722" />
<PackageVersion Include="Microsoft.DotNet.Build.Tasks.Installers" Version="8.0.0-beta.23371.1" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides extension methods for adding Azure Cosmos DB resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class AzureCosmosDBCloudApplicationBuilderExtensions
{
/// <summary>
/// Adds an Azure Cosmos DB connection to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="connectionString">The connection string.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{AzureCosmosDatabaseResource}"/>.</returns>
public static IResourceBuilder<AzureCosmosDBConnectionResource> 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);
}

/// <summary>
/// Adds an Azure Cosmos DB database to a <see cref="IResourceBuilder{AzureCosmosDatabaseResource}"/>.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{AzureCosmosDatabaseResource}"/>.</returns>
public static IResourceBuilder<AzureCosmosDatabaseResource> AddDatabase(this IResourceBuilder<AzureCosmosDBConnectionResource> builder, string name)
{
var cosmosDatabase = new AzureCosmosDatabaseResource(name, builder.Resource);
return builder
.ApplicationBuilder
.AddResource(cosmosDatabase)
.WithAnnotation(new ManifestPublishingCallbackAnnotation(
(json) => WriteCosmosDBDatabaseToManifest(json, cosmosDatabase)));
}
}
21 changes: 21 additions & 0 deletions src/Aspire.Hosting.Azure/AzureCosmosDBConnectionResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a connection to an Azure Cosmos DB account.
/// </summary>
/// <param name="name">The resource name.</param>
/// <param name="connectionString">The connection string to use to connect.</param>
public class AzureCosmosDBConnectionResource(string name, string? connectionString)
: Resource(name), IResourceWithConnectionString
{
/// <summary>
/// Gets the connection string to use for this database.
/// </summary>
/// <returns>The connection string to use for this database.</returns>
public string? GetConnectionString() => connectionString;
}
29 changes: 29 additions & 0 deletions src/Aspire.Hosting.Azure/AzureCosmosDatabaseResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents an Azure Cosmos DB database.
/// </summary>
/// <param name="name">The database name.</param>
/// <param name="parent">The parent <see cref="AzureCosmosDBConnectionResource"/>.</param>
public class AzureCosmosDatabaseResource(string name, AzureCosmosDBConnectionResource parent)
: Resource(name), IResourceWithParent<AzureCosmosDBConnectionResource>, IResourceWithConnectionString
{
/// <summary>
/// Gets the parent <see cref="AzureCosmosDBConnectionResource"/>.
/// </summary>
public AzureCosmosDBConnectionResource Parent { get; } = parent;

/// <summary>
/// Gets the connection string to use for this database.
/// </summary>
/// <returns>The connection string to use for this database.</returns>
public string? GetConnectionString()
{
return Parent.GetConnectionString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
<IsPackable>true</IsPackable>
<EnableConfigurationBindingGenerator>false</EnableConfigurationBindingGenerator>
<IsAotCompatible>false</IsAotCompatible>
<PackageTags>$(ComponentAzurePackageTags) cosmos cosmosdb data database db</PackageTags>
<Description>A client for Azure Cosmos DB that integrates with Aspire, including logging and telemetry.</Description>
<PackageIconFullPath>$(SharedDir)AzureCosmosDB_256x.png</PackageIconFullPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Azure.Cosmos" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Azure CosmosDB extension
/// </summary>
public static class AspireAzureCosmosDBExtensions
{
private const string DefaultConfigSectionName = "Aspire:Microsoft:Azure:Cosmos";

/// <summary>
/// Registers <see cref="CosmosClient" /> as a singleton in the services provided by the <paramref name="builder"/>.
/// Configures logging and telemetry for the <see cref="CosmosClient" />.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
/// <param name="connectionName">The connection name to use to find a connection string.</param>
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="AzureCosmosDBSettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section.</remarks>
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
public static void AddAzureCosmosDB(
this IHostApplicationBuilder builder,
string connectionName,
Action<AzureCosmosDBSettings>? configureSettings = null,
Action<CosmosClientOptions>? configureClientOptions = null)
{
AddAzureCosmosDB(builder, DefaultConfigSectionName, configureSettings, configureClientOptions, connectionName, serviceKey: null);
}

/// <summary>
/// Registers <see cref="CosmosClient" /> as a singleton for given <paramref name="name" /> in the services provided by the <paramref name="builder"/>.
/// Configures logging and telemetry for the <see cref="CosmosClient" />.
/// </summary>
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
/// <param name="name">The name of the component, which is used as the <see cref="ServiceDescriptor.ServiceKey"/> of the service and also to retrieve the connection string from the ConnectionStrings configuration section.</param>
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="AzureCosmosDBSettings"/>. It's invoked after the settings are read from the configuration.</param>
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section.</remarks>
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
public static void AddKeyedAzureCosmosDB(
this IHostApplicationBuilder builder,
string name,
Action<AzureCosmosDBSettings>? configureSettings = null,
Action<CosmosClientOptions>? configureClientOptions = null)
{
AddAzureCosmosDB(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, configureClientOptions, connectionName: name, serviceKey: name);
}

private static void AddAzureCosmosDB(
this IHostApplicationBuilder builder,
string configurationSectionName,
Action<AzureCosmosDBSettings>? configureSettings,
Action<CosmosClientOptions>? 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.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// The settings relevant to accessing Azure Cosmos DB.
/// </summary>
public sealed class AzureCosmosDBSettings
{
/// <summary>
/// Gets or sets the connection string of the Azure Cosmos database to connect to.
/// </summary>
public string? ConnectionString { get; set; }

/// <summary>
/// <para>Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.</para>
/// <para>Enabled by default.</para>
/// </summary>
public bool Tracing { get; set; } = true;

/// <summary>
/// A <see cref="Uri"/> referencing the Azure Cosmos DB Endpoint.
/// This is likely to be similar to "https://{account_name}.queue.core.windows.net".
/// </summary>
/// <remarks>
/// Must not contain shared access signature.
/// Used along with <see cref="Credential"/> to establish the connection.
/// </remarks>
public Uri? AccountEndpoint { get; set; }

/// <summary>
/// Gets or sets the credential used to authenticate to the Azure Cosmos DB endpoint.
/// </summary>
public TokenCredential? Credential { get; set; }
}

Loading

0 comments on commit ac8c560

Please sign in to comment.