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 nameCockroachDB and MariaDB have no dedicated AddXDocumentStore helper (Aspire has no first-party hosting
resource for them), and they can’t be auto-detected — model the container/resource yourself and pass the kind
explicitly:
var crdb = builder.AddContainer("orders-server", "cockroachdb/cockroach") /* … expose the SQL port, model the connection string … */;var store = crdb.AsDocumentStore("orders", DocumentProviderKind.CockroachDb);// MariaDB is DocumentProviderKind.MariaDb — note a plain AddPostgres/AddMySql resource auto-detects as// Postgres/MySql, so pass CockroachDb/MariaDb explicitly to select the wire-compatible variant.Consuming 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);Typed DocumentContext on an Aspire resource
Section titled “Typed DocumentContext on an Aspire resource”If you use the source-generated typed DocumentContext (EF-style
DocumentContext + DocumentSet<T>), point its store at an Aspire-provisioned resource with
AddDocumentContextProvider. It resolves the same injected connection string + provider discriminator,
wires the health check + OpenTelemetry (honoring the same settings), and returns the
Action<DocumentStoreOptions> you hand to the generated Add{Context} / Add{Context}Factory method:
// scoped (ASP.NET Core):builder.Services.AddOrdersContext(builder.AddDocumentContextProvider("orders"));
// factory (MAUI / Blazor / desktop / background):builder.Services.AddOrdersContextFactory(builder.AddDocumentContextProvider("orders"));The returned action runs inside the generated method’s configure delegate — after the context’s
ConfigureModel — so it just supplies the Aspire-resolved provider on top of your model mappings. It
takes the same optional configureSettings / configureOptions as AddDocumentStore:
builder.Services.AddOrdersContext(builder.AddDocumentContextProvider( "orders", configureSettings: s => s.DisableHealthChecks = true, configureOptions: o => o.UseReflectionFallback = false));Each context registers its store keyed by the context type, so calling AddDocumentContextProvider
once per context (each with its own Aspire name) is the multi-store story — multiple contexts, each
backed by a different Aspire resource, coexist without shadowing:
builder.Services.AddOrdersContext(builder.AddDocumentContextProvider("orders"));builder.Services.AddInvoicesContext(builder.AddDocumentContextProvider("invoices"));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 standard (core) instrumentation 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.)