Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration!How!?

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 Program.cs
var 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 discriminator

Swap the backend by changing one call — the consuming service is untouched:

builder.AddSqliteDocumentStore("orders", "orders.db"); // local dev — no container
builder.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 name

CockroachDB 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.
// Api Program.cs
builder.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.

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

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"));

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}:Provider pair 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.

  • The hosting resource implements IResourceWithConnectionString over its backing DB and publishes a provider discriminator to consumers via WithReference: env Shiny__DocumentDb__<name>__Provider / config Shiny:DocumentDb:<name>:Provider, value = the DocumentProviderKind name.
  • 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.
  • WithSeeder gates 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.

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.cs
builder.AddDocumentStore("orleans"); // Shiny.DocumentDb.Aspire.Client — keyed store + health + telemetry
builder.UseOrleans(silo => silo
.UseAspireDocumentDb("orleans")); // grain storage + reminders + clustering + grain directory

UseAspireDocumentDb 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.)