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"));Admin UI as a resource
Section titled “Admin UI as a resource”Shiny.DocumentDb.Aspire.Hosting also models the Admin UI
(ghcr.io/shinyorg/shiny-docdb-myadmin) as a resource, so it comes up with the rest of your app and every
store you reference is already connected — no connection strings to paste in by hand:
var store = builder.AddPostgresDocumentStore("orders");
builder.AddDocumentDbAdmin(port: 8085) .WithReference(store) .WaitFor(store);WithReference is the same call a consuming service makes: the tool reads the ConnectionStrings:{name}
Shiny:DocumentDb:{name}:Providerpair described below. A connection string with no matching provider key is ignored, so a Redis or blob reference in the same AppHost doesn’t turn into a junk connection. Referenced stores show up under a from host badge and can’t be edited or deleted from the UI — they’re declared in the AppHost, so that’s where they change.
Add WithDataVolume() to keep saved connections and queries across runs, and
WithHostPath(hostPath, containerPath) to make a file-backed store reachable at all. The image tag
defaults to the hosting package’s own version, so an integration upgrade brings the matching UI with it.
The full builder surface is covered in Admin UI → Aspire AppHost.
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.)


