Aspire Integration
These packages make “which database backs a DocumentDb store” and “how it gets seeded” AppHost (deployment) decisions instead of code, and give the consuming service a provider-agnostic one-liner that wires the store with health checks and OpenTelemetry already attached.
Shiny.DocumentDb.Aspire.Hosting— AppHost: model a DocumentDb store as a resource, pick its backend, and gate seeding.Shiny.DocumentDb.Aspire.Client— consuming service:AddDocumentStore("name")resolves the injected provider + connection string and registers the store.Shiny.DocumentDb.Aspire.Orleans— silo: back Orleans grain storage / reminders / clustering / grain directory with the Aspire-provisioned store in one call (see Orleans on DocumentDb).
AppHost — pick the backend + seed
Section titled “AppHost — pick the backend + seed”// AppHost Program.csvar store = builder .AddPostgresDocumentStore("orders") // provisions Postgres and models the store .WithSeeder(async (ctx, ct) => { // Runs once, after the DB is ready, before dependents start. // ctx => (StoreName, Provider, ConnectionString). DocumentDb's seeder is idempotent, // so this is the lifecycle gate, not the run-once guarantee. });
builder.AddProject<Projects.Api>("api") .WithReference(store); // injects connection string + provider discriminatorSwap the backend by changing one call — the consuming service is untouched:
builder.AddSqliteDocumentStore("orders", "orders.db"); // local dev — no containerbuilder.AddSqlServerDocumentStore("orders");builder.AddMySqlDocumentStore("orders");Layer onto a DB resource you already declared
Section titled “Layer onto a DB resource you already declared”If you already model the database, wrap it with AsDocumentStore (the provider is auto-detected from the
resource, or pass it explicitly):
var pg = builder.AddPostgres("orders-server").AddDatabase("orders-db");var store = pg.AsDocumentStore("orders"); // name MUST differ from the DB resource nameConsuming service — provider-agnostic
Section titled “Consuming service — provider-agnostic”// Api Program.csbuilder.AddDocumentStore("orders", configureOptions: o => o.MapTypeToTable<Order>());The store is registered keyed by name, so resolve it with [FromKeyedServices]:
public class OrdersService([FromKeyedServices("orders")] IDocumentStore store){ public Task<Order?> Get(string id) => store.Get<Order>(id);}AddDocumentStore reads the connection string (ConnectionStrings:orders) and the provider discriminator
(Shiny:DocumentDb:orders:Provider) the AppHost injects, selects the matching IDatabaseProvider, and —
unless disabled — registers a health check and wires the Shiny.DocumentDb meter + ActivitySource into
OpenTelemetry so query metrics and trace spans land in the Aspire dashboard.
Settings
Section titled “Settings”builder.AddDocumentStore("orders", settings =>{ settings.DisableHealthChecks = true; settings.DisableTracing = false; settings.DisableMetrics = false; // settings.Provider / settings.ConnectionString override what the AppHost injected});| Setting | Effect |
|---|---|
ConnectionString | Overrides the injected connection string |
Provider | Overrides the injected DocumentProviderKind |
DisableHealthChecks | Skips the SELECT 1 health probe registration |
DisableTracing | Skips wiring the Shiny.DocumentDb ActivitySource |
DisableMetrics | Skips wiring the Shiny.DocumentDb meter |
MultiTenant | Registers a shared-table multi-tenant store — adds a TenantId column, filters every query by the current tenant, and resolves it from a registered ITenantResolver |
Container-aware configuration
Section titled “Container-aware configuration”configureOptions handles anything that’s a plain option (JSON contexts, type/table maps, query filters,
interceptor instances). When the configuration depends on other registered services, use
configureServiceOptions — it runs with the resolved IServiceProvider when the keyed store is first
created:
builder.AddDocumentStore( "orders", configureServiceOptions: (sp, o) => o.AddInterceptor(sp.GetRequiredService<AuditInterceptor>()));For the common shared-table multi-tenancy case, just flip the MultiTenant setting — it wires
TenantIdAccessor from a registered ITenantResolver for you:
builder.Services.AddSingleton<ITenantResolver, MyTenantResolver>();builder.AddDocumentStore("orders", settings => settings.MultiTenant = true);How the pieces connect
Section titled “How the pieces connect”- The hosting resource implements
IResourceWithConnectionStringover its backing DB and publishes a provider discriminator to consumers viaWithReference: envShiny__DocumentDb__<name>__Provider/ configShiny:DocumentDb:<name>:Provider, value = theDocumentProviderKindname. - The client reads both, maps the kind to a provider (
new PostgreSqlDatabaseProvider(conn)etc.), and registers the keyed store with the standardShiny.DocumentDb.Diagnosticsinstrumentation decorator. WithSeedergates a callback on the backing resource’s ready event — the same pattern the Shiny Aspire Orleans integration uses for its database setup.
This means the provider choice and seed strategy live in the AppHost, and the consuming code is a single
provider-agnostic AddDocumentStore("name") that works regardless of which backend the AppHost picked.
Orleans on DocumentDb
Section titled “Orleans on DocumentDb”If you run Orleans persistence on DocumentDb, Shiny.DocumentDb.Aspire.Orleans
bridges the two: a silo backs all its Orleans system stores with the same Aspire-provisioned, keyed
store in one call. Register the store on the host builder, then point Orleans at it by name:
// Silo Program.csbuilder.AddDocumentStore("orleans"); // Shiny.DocumentDb.Aspire.Client — keyed store + health + telemetry
builder.UseOrleans(silo => silo .UseAspireDocumentDb("orleans")); // grain storage + reminders + clustering + grain directoryUseAspireDocumentDb wires each provider’s StoreFactory to resolve the keyed IDocumentStore, so
Orleans persistence shares the one Aspire-managed store (and its connection, health check, and telemetry).
Select a subset of features and override the provider/directory names if needed:
silo.UseAspireDocumentDb( "orleans", DocumentDbOrleansFeatures.GrainStorage | DocumentDbOrleansFeatures.Reminders, grainStorageName: "Default");On the AppHost, the silo project just references the store like any other consumer:
var store = builder.AddPostgresDocumentStore("orleans");builder.AddProject<Projects.Silo>("silo").WithReference(store);Because DocumentDb is schema-free, there are no setup scripts — the membership/storage tables are created on demand. (Clustering needs a backend with multi-document transactions — relational or MongoDB on a replica set.)