Skip to content

Commit 85566cf

Browse files
authored
Add Cosmos database builder pattern for child containers (dotnet#8266)
1 parent 1e76304 commit 85566cf

File tree

5 files changed

+411
-157
lines changed

5 files changed

+411
-157
lines changed

playground/CosmosEndToEnd/CosmosEndToEnd.ApiService/Program.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
var builder = WebApplication.CreateBuilder(args);
99

1010
builder.AddServiceDefaults();
11-
builder.AddAzureCosmosDatabase("db");
12-
builder.AddAzureCosmosContainer("entries");
11+
builder.AddAzureCosmosDatabase("db")
12+
.AddKeyedContainer("entries")
13+
.AddKeyedContainer("users");
1314
builder.AddCosmosDbContext<TestCosmosContext>("db", configureDbContextOptions =>
1415
{
1516
configureDbContextOptions.RequestTimeout = TimeSpan.FromSeconds(120);
@@ -18,14 +19,13 @@
1819
var app = builder.Build();
1920

2021
app.MapDefaultEndpoints();
21-
app.MapGet("/", async (Database db, Container container) =>
22+
23+
static async Task<object> AddAndGetStatus<T>(Container container, T newEntry)
2224
{
23-
// Add an entry to the database on each request.
24-
var newEntry = new Entry() { Id = Guid.NewGuid().ToString() };
2525
await container.CreateItemAsync(newEntry);
2626

27-
var entries = new List<Entry>();
28-
var iterator = container.GetItemQueryIterator<Entry>(requestOptions: new QueryRequestOptions() { MaxItemCount = 5 });
27+
var entries = new List<T>();
28+
var iterator = container.GetItemQueryIterator<T>(requestOptions: new QueryRequestOptions() { MaxItemCount = 5 });
2929

3030
var batchCount = 0;
3131
while (iterator.HasMoreResults)
@@ -40,10 +40,22 @@
4040

4141
return new
4242
{
43-
batchCount = batchCount,
43+
batchCount,
4444
totalEntries = entries.Count,
45-
entries = entries
45+
entries
4646
};
47+
}
48+
49+
app.MapGet("/", async ([FromKeyedServices("entries")] Container container) =>
50+
{
51+
var newEntry = new Entry() { Id = Guid.NewGuid().ToString() };
52+
return await AddAndGetStatus(container, newEntry);
53+
});
54+
55+
app.MapGet("/users", async ([FromKeyedServices("users")] Container container) =>
56+
{
57+
var newEntry = new User() { Id = $"user-{Guid.NewGuid()}" };
58+
return await AddAndGetStatus(container, newEntry);
4759
});
4860

4961
app.MapGet("/ef", async (TestCosmosContext context) =>
@@ -58,6 +70,12 @@
5870

5971
app.Run();
6072

73+
public class User
74+
{
75+
[JsonProperty("id")]
76+
public string? Id { get; set; }
77+
}
78+
6179
public class Entry
6280
{
6381
[JsonProperty("id")]

playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
.RunAsPreviewEmulator(e => e.WithDataExplorer());
1010

1111
var db = cosmos.AddCosmosDatabase("db");
12-
var container = db.AddContainer("entries", "/id");
12+
var entries = db.AddContainer("entries", "/id", "staging-entries");
13+
db.AddContainer("users", "/id");
1314

1415
builder.AddProject<Projects.CosmosEndToEnd_ApiService>("api")
1516
.WithExternalHttpEndpoints()
1617
.WithReference(db).WaitFor(db)
17-
.WithReference(container).WaitFor(container);
18+
.WithReference(entries).WaitFor(entries);
1819

1920
#if !SKIP_DASHBOARD_REFERENCE
2021
// This project is only added in playground projects to support development/debugging

src/Components/Aspire.Microsoft.Azure.Cosmos/AspireMicrosoftAzureCosmosExtensions.cs

Lines changed: 62 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,6 @@ public static void AddAzureCosmosClient(
3838
builder.Services.AddSingleton(sp => GetCosmosClient(connectionName, settings, clientOptions));
3939
}
4040

41-
/// <summary>
42-
/// Registers the <see cref="Database"/> as a singleton in the services provided by the <paramref name="builder"/>.
43-
/// </summary>
44-
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
45-
/// <param name="connectionName">The connection name to use to find a connection string.</param>
46-
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="MicrosoftAzureCosmosSettings"/>. It's invoked after the settings are read from the configuration.</param>
47-
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
48-
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section.</remarks>
49-
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
50-
public static void AddAzureCosmosDatabase(
51-
this IHostApplicationBuilder builder,
52-
string connectionName,
53-
Action<MicrosoftAzureCosmosSettings>? configureSettings = null,
54-
Action<CosmosClientOptions>? configureClientOptions = null)
55-
{
56-
var settings = builder.GetSettings(connectionName, configureSettings);
57-
var clientOptions = builder.GetClientOptions(settings, configureClientOptions);
58-
builder.Services.AddSingleton(sp =>
59-
{
60-
if (string.IsNullOrEmpty(settings.DatabaseName))
61-
{
62-
throw new InvalidOperationException($"The connection string '{connectionName}' does not exist or is missing the database name.");
63-
}
64-
CosmosClient? client = null;
65-
if (configureClientOptions is null)
66-
{
67-
client = sp.GetService<CosmosClient>();
68-
}
69-
client ??= GetCosmosClient(connectionName, settings, clientOptions);
70-
return client.GetDatabase(settings.DatabaseName);
71-
});
72-
}
73-
7441
/// <summary>
7542
/// Registers the <see cref="Container"/> as a singleton in the services provided by the <paramref name="builder"/>.
7643
/// </summary>
@@ -79,6 +46,15 @@ public static void AddAzureCosmosDatabase(
7946
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="MicrosoftAzureCosmosSettings"/>. It's invoked after the settings are read from the configuration.</param>
8047
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
8148
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos" section.</remarks>
49+
/// <remarks>
50+
/// The <see cref="Container"/> is registered as a singleton in the services provided by
51+
/// the <paramref name="builder"/> and does not reuse any existing <see cref="CosmosClient"/>
52+
/// instances in the DI container. The connection string associated with the <paramref name="connectionName"/>
53+
/// must contain the database name and container name or be set in the <paramref name="configureSettings" />
54+
/// callback. To interact with multiple containers against the same database, use
55+
/// <see cref="CosmosDatabaseBuilder"/> to register the database and then call
56+
/// <see cref="CosmosDatabaseBuilder.AddKeyedContainer(string)"/> for each container.
57+
/// </remarks>
8258
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
8359
public static void AddAzureCosmosContainer(
8460
this IHostApplicationBuilder builder,
@@ -94,12 +70,7 @@ public static void AddAzureCosmosContainer(
9470
{
9571
throw new InvalidOperationException($"The connection string '{connectionName}' does not exist or is missing the container name or database name.");
9672
}
97-
CosmosClient? client = null;
98-
if (configureClientOptions is null)
99-
{
100-
client = sp.GetService<CosmosClient>();
101-
}
102-
client ??= GetCosmosClient(connectionName, settings, clientOptions);
73+
var client = GetCosmosClient(connectionName, settings, clientOptions);
10374
return client.GetContainer(settings.DatabaseName, settings.ContainerName);
10475
});
10576
}
@@ -132,72 +103,89 @@ public static void AddKeyedAzureCosmosClient(
132103
}
133104

134105
/// <summary>
135-
/// Registers the <see cref="Database"/> as a singleton for given <paramref name="name" /> in the services provided by the <paramref name="builder"/>.
106+
/// Registers the <see cref="Container"/> as a singleton for given <paramref name="name" /> in the services provided by the <paramref name="builder"/>.
136107
/// </summary>
137108
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
138109
/// <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>
139110
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="MicrosoftAzureCosmosSettings"/>. It's invoked after the settings are read from the configuration.</param>
140111
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
141112
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section.</remarks>
113+
/// <remarks>
114+
/// The <see cref="Container"/> is registered as a singleton in the services provided by
115+
/// the <paramref name="builder"/> and does not reuse any existing <see cref="CosmosClient"/>
116+
/// instances in the DI container. The connection string associated with the <paramref name="name"/>
117+
/// must contain the database name and container name or be set in the <paramref name="configureSettings" />
118+
/// callback. To interact with multiple containers against the same database, use
119+
/// <see cref="CosmosDatabaseBuilder"/> to register the database and then call
120+
/// <see cref="CosmosDatabaseBuilder.AddKeyedContainer(string)"/> for each container.
121+
/// </remarks>
142122
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
143-
public static void AddKeyedAzureCosmosDatabase(
144-
this IHostApplicationBuilder builder,
145-
string name,
146-
Action<MicrosoftAzureCosmosSettings>? configureSettings = null,
147-
Action<CosmosClientOptions>? configureClientOptions = null)
123+
public static void AddKeyedAzureCosmosContainer(
124+
this IHostApplicationBuilder builder,
125+
string name,
126+
Action<MicrosoftAzureCosmosSettings>? configureSettings = null,
127+
Action<CosmosClientOptions>? configureClientOptions = null)
148128
{
149129
var settings = builder.GetSettings(name, configureSettings);
150130
var clientOptions = builder.GetClientOptions(settings, configureClientOptions);
151131
builder.Services.AddKeyedSingleton(name, (sp, key) =>
152132
{
153-
if (string.IsNullOrEmpty(settings.DatabaseName))
154-
{
155-
throw new InvalidOperationException($"The connection string '{name}' does not exist or is missing the database name.");
156-
}
157-
CosmosClient? client = null;
158-
if (configureClientOptions is null)
133+
if (string.IsNullOrEmpty(settings.ContainerName) || string.IsNullOrEmpty(settings.DatabaseName))
159134
{
160-
client = sp.GetKeyedService<CosmosClient>(key);
135+
throw new InvalidOperationException($"The connection string '{name}' does not exist or is missing the container name or database name.");
161136
}
162-
client ??= GetCosmosClient(name, settings, clientOptions);
163-
return client.GetDatabase(settings.DatabaseName);
137+
var client = GetCosmosClient(name, settings, clientOptions);
138+
return client.GetContainer(settings.DatabaseName, settings.ContainerName);
164139
});
165140
}
166141

167142
/// <summary>
168-
/// Registers the <see cref="Container"/> as a singleton for given <paramref name="name" /> in the services provided by the <paramref name="builder"/>.
143+
/// Registers the <see cref="Database"/> as a singleton the services provided by the <paramref name="builder"/>
144+
/// and returns a <see cref="CosmosDatabaseBuilder"/> to support chaining multiple container registrations against the same database.
169145
/// </summary>
170146
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
171-
/// <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>
147+
/// <param name="connectionName">The connection name to use to find a connection string.</param>
172148
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="MicrosoftAzureCosmosSettings"/>. It's invoked after the settings are read from the configuration.</param>
173149
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
174150
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section.</remarks>
175151
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
176-
public static void AddKeyedAzureCosmosContainer(
152+
public static CosmosDatabaseBuilder AddAzureCosmosDatabase(
177153
this IHostApplicationBuilder builder,
178-
string name,
154+
string connectionName,
179155
Action<MicrosoftAzureCosmosSettings>? configureSettings = null,
180156
Action<CosmosClientOptions>? configureClientOptions = null)
157+
{
158+
var settings = builder.GetSettings(connectionName, configureSettings);
159+
var clientOptions = builder.GetClientOptions(settings, configureClientOptions);
160+
var cosmosDatabaseBuilder = new CosmosDatabaseBuilder(builder, connectionName, settings, clientOptions);
161+
cosmosDatabaseBuilder.AddDatabase();
162+
return cosmosDatabaseBuilder;
163+
}
164+
165+
/// <summary>
166+
/// Registers the <see cref="Database"/> as a singleton for given <paramref name="name" /> in the services provided by the <paramref name="builder"/>
167+
/// and returns a <see cref="CosmosDatabaseBuilder"/> to support chaining multiple container registrations against the same database.
168+
/// </summary>
169+
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
170+
/// <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>
171+
/// <param name="configureSettings">An optional method that can be used for customizing the <see cref="MicrosoftAzureCosmosSettings"/>. It's invoked after the settings are read from the configuration.</param>
172+
/// <param name="configureClientOptions">An optional method that can be used for customizing the <see cref="CosmosClientOptions"/>.</param>
173+
/// <remarks>Reads the configuration from "Aspire:Microsoft:Azure:Cosmos:{name}" section.</remarks>
174+
/// <exception cref="InvalidOperationException">If required ConnectionString is not provided in configuration section</exception>
175+
public static CosmosDatabaseBuilder AddKeyedAzureCosmosDatabase(
176+
this IHostApplicationBuilder builder,
177+
string name,
178+
Action<MicrosoftAzureCosmosSettings>? configureSettings = null,
179+
Action<CosmosClientOptions>? configureClientOptions = null)
181180
{
182181
var settings = builder.GetSettings(name, configureSettings);
183182
var clientOptions = builder.GetClientOptions(settings, configureClientOptions);
184-
builder.Services.AddKeyedSingleton(name, (sp, key) =>
185-
{
186-
if (string.IsNullOrEmpty(settings.ContainerName) || string.IsNullOrEmpty(settings.DatabaseName))
187-
{
188-
throw new InvalidOperationException($"The connection string '{name}' does not exist or is missing the container name or database name.");
189-
}
190-
CosmosClient? client = null;
191-
if (configureClientOptions is null)
192-
{
193-
client = sp.GetKeyedService<CosmosClient>(key);
194-
}
195-
client ??= GetCosmosClient(name, settings, clientOptions);
196-
return client.GetContainer(settings.DatabaseName, settings.ContainerName);
197-
});
183+
var cosmosDatabaseBuilder = new CosmosDatabaseBuilder(builder, name, settings, clientOptions);
184+
cosmosDatabaseBuilder.AddKeyedDatabase();
185+
return cosmosDatabaseBuilder;
198186
}
199187

200-
private static CosmosConnectionInfo? GetCosmosConnectionInfo(this IHostApplicationBuilder builder, string connectionName)
188+
internal static CosmosConnectionInfo? GetCosmosConnectionInfo(this IHostApplicationBuilder builder, string connectionName)
201189
{
202190
ArgumentNullException.ThrowIfNull(builder);
203191
ArgumentException.ThrowIfNullOrEmpty(connectionName);
@@ -275,7 +263,7 @@ private static CosmosClientOptions GetClientOptions(
275263
return clientOptions;
276264
}
277265

278-
private static CosmosClient GetCosmosClient(string connectionName, MicrosoftAzureCosmosSettings settings, CosmosClientOptions clientOptions)
266+
internal static CosmosClient GetCosmosClient(string connectionName, MicrosoftAzureCosmosSettings settings, CosmosClientOptions clientOptions)
279267
{
280268
if (!string.IsNullOrEmpty(settings.ConnectionString))
281269
{
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Azure.Cosmos;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Hosting;
7+
8+
namespace Aspire.Microsoft.Azure.Cosmos;
9+
10+
/// <summary>
11+
/// Represents a builder that can be used to register multiple container
12+
/// instances against the same Cosmos database connection.
13+
/// </summary>
14+
public sealed class CosmosDatabaseBuilder(
15+
IHostApplicationBuilder hostBuilder,
16+
string connectionName,
17+
MicrosoftAzureCosmosSettings settings,
18+
CosmosClientOptions clientOptions)
19+
{
20+
private CosmosClient? _client;
21+
22+
internal CosmosDatabaseBuilder AddDatabase()
23+
{
24+
hostBuilder.Services.AddSingleton(sp =>
25+
{
26+
if (string.IsNullOrEmpty(settings.DatabaseName))
27+
{
28+
throw new InvalidOperationException(
29+
$"A Database could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}'.");
30+
}
31+
_client ??= AspireMicrosoftAzureCosmosExtensions.GetCosmosClient(connectionName, settings, clientOptions);
32+
return _client.GetDatabase(settings.DatabaseName);
33+
});
34+
35+
return this;
36+
}
37+
38+
internal CosmosDatabaseBuilder AddKeyedDatabase()
39+
{
40+
hostBuilder.Services.AddKeyedSingleton(connectionName, (sp, _) =>
41+
{
42+
if (string.IsNullOrEmpty(settings.DatabaseName))
43+
{
44+
throw new InvalidOperationException(
45+
$"A Database could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}'.");
46+
}
47+
_client ??= AspireMicrosoftAzureCosmosExtensions.GetCosmosClient(connectionName, settings, clientOptions);
48+
return _client.GetDatabase(settings.DatabaseName);
49+
});
50+
51+
return this;
52+
}
53+
54+
/// <summary>
55+
/// Register a <see cref="Container"/> against the database managed with <see cref="CosmosDatabaseBuilder"/> as a
56+
/// keyed singleton.
57+
/// </summary>
58+
/// <param name="name">The name of the container to register.</param>
59+
/// <returns>A <see cref="CosmosDatabaseBuilder"/> that can be used for further chaining.</returns>
60+
public CosmosDatabaseBuilder AddKeyedContainer(string name)
61+
{
62+
_client ??= AspireMicrosoftAzureCosmosExtensions.GetCosmosClient(connectionName, settings, clientOptions);
63+
64+
var connectionInfo = hostBuilder.GetCosmosConnectionInfo(name);
65+
66+
hostBuilder.Services.AddKeyedSingleton(name, (sp, _) =>
67+
{
68+
// If a connection string was provided, check that it contains a valid container name.
69+
if (connectionInfo is not null && string.IsNullOrEmpty(connectionInfo?.ContainerName))
70+
{
71+
throw new InvalidOperationException(
72+
$"A Container could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{name}'");
73+
}
74+
75+
// Use the container name from the connection string if provided, otherwise use the name
76+
return _client.GetContainer(settings.DatabaseName, connectionInfo?.ContainerName ?? name);
77+
});
78+
79+
return this;
80+
}
81+
}

0 commit comments

Comments
 (0)