Skip to content

Instantly share code, notes, and snippets.

@jmezach
Created October 21, 2025 07:57
Show Gist options
  • Select an option

  • Save jmezach/08ec8e54aea2b531bdb661f0949bc0c3 to your computer and use it in GitHub Desktop.

Select an option

Save jmezach/08ec8e54aea2b531bdb661f0949bc0c3 to your computer and use it in GitHub Desktop.
.NET Aspire database-per-tenant
var builder = DistributedApplication.CreateBuilder(args);
var configurationDatabase = builder.AddConnectionString("ConfigurationDatabase");
var databasePerTenant = builder.AddTenantedConnectionString("DatabasePerTenant")
.WithReference(configurationDatabase);
var api = builder.AddProject<Projects.Api>("api")
.WithReference(databasePerTenant);
builder.Build().Run();
using Microsoft.Data.SqlClient;
namespace Aspire.Hosting;
public static class TenantedConnectionStringBuilderExtensions
{
public static IResourceBuilder<TenantedConnectionStringResource> AddTenantedConnectionString(this IDistributedApplicationBuilder builder, string name)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
var tenantedConnectionString = new TenantedConnectionStringResource(name);
return builder.AddResource(tenantedConnectionString);
}
public static IResourceBuilder<TenantedConnectionStringResource> WithReference<TSource>(this IResourceBuilder<TenantedConnectionStringResource> builder, IResourceBuilder<TSource> connectionStringResource)
where TSource : IResourceWithConnectionString
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(connectionStringResource);
connectionStringResource.OnResourceReady(async (resource, @event, ct) =>
{
try
{
Console.WriteLine($"Loading tenanted connection strings for {builder.Resource.Name}...");
await LoadTenantedConnectionStringsAsync(builder, resource, ct);
Console.WriteLine($"Finished loading tenanted connection strings for {builder.Resource.Name}: {builder.Resource.TenantConnectionStrings.Count} tenants found.");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading tenanted connection strings for {builder.Resource.Name}: {ex}");
throw;
}
});
return builder;
}
private static async Task LoadTenantedConnectionStringsAsync(IResourceBuilder<TenantedConnectionStringResource> tenantedBuilder, IResourceWithConnectionString sourceResource, CancellationToken ct)
{
// I'll leave this an exercise to the reader as it is highly application specific
// What's import though is that all relevant connection strings are added to the TenantedConnectionStringResource like so:
// tenantedBuilder.Resource.TenantConnectionStrings.Add($"{tenantedBuilder.Resource.Name}-{tenantId}", connectionString);
}
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<TenantedConnectionStringResource> tenantedConnectionStringResource)
where TDestination : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(tenantedConnectionStringResource);
builder.WithEnvironment(async context =>
{
tenantedConnectionStringResource.Resource.TenantConnectionStrings.ToList().ForEach(kvp =>
{
context.EnvironmentVariables[$"ConnectionStrings__{kvp.Key}"] = kvp.Value;
});
});
return builder;
}
}
namespace Aspire.Hosting.ApplicationModel;
public class TenantedConnectionStringResource([ResourceName]string name) : Resource(name)
{
public string Name { get; } = name;
internal Dictionary<string, string> TenantConnectionStrings { get; } = new();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment