Document DB
A lightweight, database-agnostic document store for .NET that turns your database into a schema-free JSON document database with LINQ querying, spatial/geo queries, vector / ANN search, and full AOT/trimming support. Store entire object graphs — nested objects, child collections — as JSON documents. No CREATE TABLE, no ALTER TABLE, no JOINs, no migrations. One API, multiple database providers.
Packages
Section titled “Packages”| GitHub | |
Core (Shiny.DocumentDb) |
Providers
Section titled “Providers”Install one — it brings the core package with it. Provider reference
Integrations
Section titled “Integrations”MCP server (dotnet tool) |
shiny-documentdb-mcp |
|
Admin terminal UI (dotnet tool) |
shinydocdb |
The web admin UI ships as a container image — ghcr.io/shinyorg/shiny-docdb-myadmin. Admin UI
Features
Section titled “Features”- Multi-provider — SQLite, SQLCipher (encrypted SQLite), LiteDB, IndexedDB (Blazor WASM), DuckDB, PostgreSQL, CockroachDB, SQL Server, MySQL, MariaDB, Oracle, CosmosDB, MongoDB, Amazon DocumentDB, Azure Table Storage, Amazon DynamoDB, Redis, RavenDB, and Google Firestore with a single API. Provider reference
- Zero schema, zero migrations — store objects as JSON documents
- Fluent query builder —
store.Query<User>().Where(u => u.Age > 30).OrderBy(u => u.Name).Paginate(0, 20).ToList()with full LINQ expression support for nested properties,Any(),Count(), string methods, null checks, and captured variables IAsyncEnumerable<T>streaming — yield results one-at-a-time with.ToAsyncEnumerable()- Expression-based JSON indexes — up to 30x faster queries on indexed properties
- SQL-level projections — project into DTOs via
.Select()at the database level - Aggregates — scalar
.Max(),.Min(),.Sum(),.Average()as terminal methods; aggregate projections with automatic GROUP BY viaSql.*markers; collection-level Sum, Min, Max, Average on child collections - Grouped aggregation —
Query<Order>().GroupBy(o => o.Status).Having(g => g.Count() > 5).Select(g => new { g.Key, Revenue = g.Sum(o => o.Total) })with multi-part and derived keys, plus a string form (GroupBy("customer.tier")). Pushed down to real SQLGROUP BY/HAVINGon the relational providers and evaluated client-side on MongoDB, Cosmos, LiteDB, and IndexedDB. Learn more - Ordering —
.OrderBy(u => u.Age)and.OrderByDescending(u => u.Name)on the fluent query builder - Pagination —
.Paginate(offset, take)translates to SQLLIMIT/OFFSET - Single-row terminals —
First,FirstOrDefault,Single, andSingleOrDefaultend a query with one document, with predicate (First(x => x.Age == 40)) and string-grammar (First("status == 'open'")) overloads. Not sugar overToList()— the row limit reaches the provider, so a relational store emitsLIMIT 1and MongoDB/Cosmos page server-side. Every provider. Learn more - Raw JSON results — end a typed
Query<T>()with JSON instead ofT, so a document that only has to reach an HTTP response never becomes an object:FirstOrDefaultRawJson(),WriteJsonArrayTo(stream)(streamed, never buffered), plus aJsonObjectlane (ToJsonList,ToJsonAsyncEnumerable,FirstJson,ToJsonCursorPage). The full typed builder still applies —Where,OrderBy,Paginate, query filters, soft delete, tenancy. Relational providers and Cosmos hand back the persisted body untouched. Learn more - Table-per-type mapping —
cfg.Table = "orders"inside aConfigureDocument<T>block gives a document type its own dedicated table. Unmapped types share a configurable default table - Custom Id properties —
cfg.MapIdProperty(x => x.MyProp)overrides the Id, on its own or alongsidecfg.Table - Document diffing —
GetDiffcompares a modified object against the stored document and returns an RFC 6902JsonPatchDocument<T>with deep nested-object diffing - Surgical field updates —
SetPropertyupdates a single JSON field without deserialization.RemovePropertystrips a field. Both support nested paths - JSON Merge Patch (Upsert) —
Upsertuses RFC 7396json_patchto deep-merge a partial object into an existing document, preserving unset nullable fields. Inserts if the document doesn’t exist - Merge-vs-replace flags — pick the write mode explicitly without switching methods:
Update(doc, patch: true)deep-merges instead of full-replacing, andUpsert(doc, patchIfUpdate: false)replaces the body wholesale instead of merging. The same flags apply on a JSON collection (Collection(type).Update(jsonObject, patch: true)), which is the precise way to do partial updates. Relational providers; non-default modes throwNotSupportedExceptionon the document-native and key-partitioned stores. Learn more - Bulk operations —
Query<T>().Where(...).ExecuteUpdate(x => x.Prop, value)and.ExecuteDelete()issue a single SQL statement against all matching documents — no deserialization, no client-side loop.ExecuteUpdate(b => b.Set(o => o.Status, "expired").Set(o => o.ClosedAt, now))sets several properties in one atomic statement - Typed Id lookups —
Get,Remove,SetProperty, andRemovePropertyaccept the Id asobjectso you can pass aGuid,int,long, orstringdirectly. Unsupported types throwArgumentException - JSON collections — read and write documents as raw JSON through one API, addressed either by name (schema-free — no CLR type at all) or by CLR type (late-bound — a registered type, but no instance):
store.Collection("orders")/store.Collection(typeof(Order)). Both giveInsert/Update/Upsert/Get/Remove/Clearplus a fluent string-grammarQuery()returningJsonObjects. A type-keyed collection stores the body AS-IS but rides the full write pipeline (tenancy, temporal, versioning/CAS, spatial + vector sidecars, interceptors, change notifications) and resolves paths through the type’s metadata; a name-keyed one has no registrations at all and infers field types from the query (with an explicittotal:numberhint where nothing else pins them). Ideal for generic HTTP intake, message-bus payloads, ETL, gateways, and genuinely schema-free data. Relational providers. Learn more - Typed
DocumentContext— an optional, EF-Core-style typed front-end overIDocumentStore. Declare aggregates on apartialcontext with[Document(typeof(User), Id = nameof(User.Email), JsonContext = typeof(AppJsonContext))]and a bundled source generator emits aDocumentSet<T>per type plus DI sugar (AddAppContext(...)scoped,AddAppContextFactory(...)for the MAUI/Blazor/desktop story). Work model-first —await db.Users.Where(u => u.Age >= 18).ToList()— withJsonTypeInfo<T>threaded automatically. Astatic partial void OnConfiguring(DocumentModelBuilder model)hook lets the context declare its whole document model next to its[Document]list instead of insideAddDocumentStore. Ships in core, works over all providers. Learn more - Computed properties — map a value derived from other fields (
Total = Quantity * UnitPrice, a normalizedlower(Email)) that you filter, sort, and project by exactly like a stored property, though it’s never written into the JSON.cfg.MapComputedProperty(o => o.Total, o => o.Quantity * o.UnitPrice)runs in alias mode by default (SQL-inlined, zero schema); passindexed: trueon a relational provider to materialize it as a native generated/computed column + index. Recomputed and written back onto the object on read. Fully AOT/trim-safe. Learn more - Full AOT/trimming support — all
JsonTypeInfo<T>parameters are optional and auto-resolve from a configuredJsonSerializerContext. SetUseReflectionFallback = falseto catch missing registrations with clear exceptions - Validate-on-build — one configuration sweep when the store is constructed, reporting every problem together through
DocumentConfigurationExceptioninstead of one per restart. It catches features the chosen backend doesn’t have (a vector mapping on LiteDB,cfg.Tableon RavenDB) and randomized-encrypted properties used where the database has to read through them.DocumentConfigurationValidator.Collect(options)returns the same list without throwing - Optimistic concurrency —
cfg.MapVersionProperty(x => x.RowVersion)enables automatic version checking on update/upsert. Version is set to 1 on insert, checked and incremented on update. ThrowsConcurrencyExceptionon conflict. Works across all providers — stored in the JSON blob with zero schema changes - Unit of work —
store.OpenSession()+SaveChanges()with automatic commit/rollback.IDocumentSessionis the scoped, per-request front end (AddScopedDocumentSession());IDocumentSessionFactoryopens one where there’s no ambient DI scope (MAUI/desktop, workers, Orleans grains, seeders) and resolves named stores withGetStore("name") - Batch writes —
BatchInsertinserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure.BatchUpsert,BatchUpdate, andBatchRemove<T>(ids)apply many writes as one set operation (a single multi-rowINSERT … ON CONFLICTdeep-merge on SQLite/DuckDB, oneBulkWrite/DeleteManyon MongoDB, parallel request waves on Cosmos, a singleDELETE … IN (…)on relational). All-or-nothing — the first version conflict rolls the whole batch back - Spatial / geo queries (full OGC geometry) — beyond point-only
WithinRadius/WithinBoundingBox/NearestNeighbors, a fullGeometrymodel (GeoLineString,GeoPolygonwith holes, multi-geometries) maps viacfg.MapSpatialProperty(x => x.Area)and queries with the topological predicate family —GeoIntersects,GeoContains,GeoWithin,GeoCovers,GeoWithinDistance, and friends — returningSpatialResult<T>withDistanceMeters. Compose spatial predicates inside ordinary LINQ withDocumentFunctions.Intersects/Distance/…(server-side, combinable with otherWhere/OrderBy/paging) and in the string-expression surface too. Backed by a real 2-D spatial index on every SQL provider — SQLite R*Tree, PostgreSQL GiST, MySQLSPATIAL, DuckDB R-Tree, SQL Server spatial index, OracleSDO_GEOMETRY+ MDSYS — plus nativeST_*on CosmosDB and2dsphereon MongoDB. Learn more - Vector / ANN search — register an embedding property with
cfg.MapVectorProperty(d => d.Embedding, dimensions: 1536, metric: VectorDistance.Cosine)and query withQuery<T>().Where(...).NearestVectors(query, k). Provider-native indexes: pgvector (PostgreSQL),VECTOR+ DiskANN (SQL Server 2025), nativeVECTOR+ HNSW/IVF (Oracle 23ai), embedding policy (CosmosDB),$vectorSearch(MongoDB Atlas),vssextension (DuckDB),sqlite-vec(SQLite). PlusAutoEmbedOnInsert<T>to plug inMicrosoft.Extensions.AI.IEmbeddingGeneratorand embed text automatically on every write. Learn more - Full-text search (all providers) —
cfg.MapFullTextProperty(a => a.Body)(or an array of paths) +store.FullTextSearch<T>("orleans persistence")for relevance-ranked search, returningFullTextResult<T>(Document+ normalizedScore) ordered by relevance, with an optional pre-filter and a fluentstore.Query<T>().Where(...).FullTextMatch("...")form. The native index is auto-created and engine-maintained: FTS5 (SQLite),tsvector+GIN (PostgreSQL),FULLTEXT(MySQL), Oracle Text, SQL Server Full-Text, theftsextension (DuckDB), full-text policy (CosmosDB),$text(MongoDB), and an in-memory TF-IDF fallback on LiteDB / IndexedDB. A type must be mapped before it can be searched. Learn more - Composite JSON indexes —
CreateIndexAsync(ctx.User, u => u.Country, u => u.Age)builds a single B-tree across multiple JSON paths on SQLite, SQLCipher, PostgreSQL, MySQL, Oracle, DuckDB, and SQL Server. Learn more - Hot backup —
Backupcopies the database to a file. Available onSqliteDocumentStore,SqlCipherDocumentStore, andLiteDbDocumentStore - Streaming bulk export / import / restore — the
IDocumentBackupstore capability moves a whole store’s contents in and out as a portable, streamed v1 backup:ExportAsync(Stream),RestoreAsync(Stream), and the lower-levelBulkImportAsync(IAsyncEnumerable<RawDocument>). Bodies bound verbatim (no<T>, no reflection — AOT-friendly);BulkWriteModepicks Insert/Replace/Merge/SkipExisting, with a native bulk-copy fast path (10-100× faster) on PostgreSQLCOPY, SQL ServerSqlBulkCopy, and DuckDB appender. Every SQL provider plus MongoDB and Cosmos. Learn more - Clear the whole store —
((IDocumentMaintenance)store).ClearAll()wipes every document type plus temporal-history, spatial, and vector sidecars (test/dev resets) without touching the system catalogs. Implemented on the relationalDocumentStore(SQLite, SQL Server, PostgreSQL, MySQL, DuckDB, Oracle), MongoDB, and CosmosDB; the olderSqliteDocumentStore.ClearAllAsync()delegates to it - Database seeding — register
IDocumentSeeders to populate initial data once at startup. Schema-free seeding is just idempotent writes, so seeders are provider-agnostic; run-once is versioned via aDocumentSeedMarker(bumpVersionto re-run). Wire withAddDocumentSeeder<T>()/AddDocumentSeeder(name, version, delegate)at host startup, or callDocumentSeedRunner.RunAsync(store, seeders)directly (e.g. on MAUI) - SQLCipher encryption — separate
Shiny.DocumentDb.Sqlite.SqlCipherpackage with AES-256 encryption, password-aware backup, andRekeyAsyncto change the encryption key - Field-level encryption — in the core package, on every provider.
opts.UseEncryptor(new AesGcmDocumentEncryptor("k1", key))pluscfg.MapProperty(x => x.Ssn, p => p.Encrypt())makes a property AES-256-GCM ciphertext everywhere it’s stored, with no change to how documents are read or written. Installed as aJsonTypeInfomodifier, so every write path — temporal history, backup export — is covered by construction and no provider needs to know about it.EncryptionMode.Deterministickeeps equality filters working by rewriting the predicate’s constant into ciphertext; anything unanswerable against ciphertext throws with an explanation instead of matching nothing. Key rotation is a key ring plusRewrapAsync<T>(), and values written before the property was mapped keep reading, so it can be turned on for a populated store. AOT-clean, no new dependency. Learn more - Multi-tenancy — two isolation strategies: shared-table (single database with automatic
TenantIdcolumn filtering) and tenant-per-database (a store per tenant on any provider, built on first use and held in a bounded cache with per-tenant seeding, eviction control and per-tenant telemetry). Both resolve the current tenant via a user-implementedITenantResolver. Consumer code is unchanged — tenant isolation is applied transparently - Change monitoring — consume an
IAsyncEnumerable<DocumentChange<T>>of insert/update/remove/clear events withawait foreach (var c in store.NotifyOnChange<User>(ct)). Filter to a single document withWhenDocumentChanged<T>(id)or to the result set of a fluent query withquery.NotifyOnChange(). Buffered in a session and emitted on commit. Learn more - Native change feeds —
IChangeFeedDocumentStore.SubscribeChanges<T>observes all writers via the database’s own mechanism: PostgreSQLLISTEN/NOTIFYtriggers, SQL Server Change Tracking (optionally withSqlDependencyquery notifications), and Cosmos DB native Change Feed. Provisioning is automatic and idempotent - Temporal history (system-time versioning) —
cfg.MapTemporal(o => { o.Retention = ...; o.MaxVersions = ...; o.CaptureActor = ...; })opts a type into append-only versioning. Every Insert/Update/Upsert/Remove/SetProperty/RemoveProperty/BatchInsert records a snapshot to a per-type history sidecar. Read it back withHistory<T>(id),AsOf<T>(id, when),Restore<T>(id, version),GetDiffBetween<T>(id, from, to), plus fleet-wideAsOfAll<T>(when),ChangesByActor<T>(actor), andChangesBetween<T>(from, to)— on theITemporalDocumentStorecapability interface, notIDocumentStore. Opt-in per type, on every provider (relational and document/NoSQL). Learn more - Write interceptors — observe and mutate writes as they happen, per-document (
IDocumentInterceptor—Insert,BatchInsertper item,Update,Upsert,Remove) or set-based (IDocumentBulkInterceptor—ExecuteUpdate,ExecuteDelete,Clear). The after-hook runs inside the same transaction, after the write succeeds and before commit, so it sees the generated id/version and can do transactional side effects like an outbox. A before-hook can alsoctx.Cancel()to replace the write entirely. Every provider. Learn more - Transactional outbox — record “this happened” in the same transaction as the write that made it happen, with no second datastore and no dual-write window:
o.AddOutbox()+services.AddDocumentOutbox<BusDispatcher>(), thensession.Add(order).Enqueue(new OrderPlaced(...))— both rows commit together or neither does. Or declaratively per type withcfg.PublishToOutbox(ctx => new OrderChanged(...)). At-least-once delivery with attempt counters, exponential backoff and dead-lettering; claiming is per-message optimistic concurrency so workers scale by just running.store.WatchOutbox(...)streams for dashboards,IOutboxAdminis the operational view, and thetraceparentcaptured at enqueue is restored at dispatch. Relational providers and LiteDB — everything else implements a unit of work by compensation and is gated out by name at startup viaIDocumentStore.SupportsTransactions. Learn more - Soft delete —
cfg.AddSoftDelete(x => x.IsDeleted)andRemove,ExecuteDelete, andClearall set the flag instead of deleting, while every read hides the flagged documents (IncludeDeleted()opts back in). Nothing is wired into the stores — it’s a global query filter plus a write-cancelling interceptor, the same two public building blocks you’d use to write your own variant. Learn more - Blobs — attach binary payloads (a PDF, an image, a signature) to a document via
DocumentBlob/DocumentBlobCollection, stored in a sidecar table and loaded on demand. The body keeps only metadata (size, content type, file name), so ordinary reads, queries, the change feed, and temporal history never drag the bytes along — unlike a rawbyte[], which base64-inflates theDatacolumn ~33% and materializes on every read. Learn more - Reference geo data —
Shiny.DocumentDb.Geoships an embedded dataset of US states, Canadian provinces, and US & Canadian cities that seeds straight into any store: point-in-region lookups, nearest-city queries, and population data without wiring up an external gazetteer. Plain documents through the normal seeding + spatial machinery, so it’s provider-agnostic. Learn more - Global query filters — register a
cfg.AddQueryFilter(u => !u.IsDeleted)predicate that’s automatically AND-applied to every query ofT, plusGet/Update/Remove/SetProperty/RemoveProperty/Clear/ExecuteUpdate/ExecuteDeleteand per-query change monitoring. Mirrors Entity Framework Core’sHasQueryFilter, including named filters,IgnoreQueryFilters()/IgnoreQueryFilters("name"), and captured-variable semantics. Learn more - AI tool integration —
Shiny.DocumentDb.Extensions.AIexposes document types asMicrosoft.Extensions.AItool functions for LLM agents. Per-type capability flags (ReadOnly,All), structured filter expressions, field visibility control, and page size caps. A non-removable per-typeWherescope answers “which documents may this caller see” — statically, or resolved per tool call from the call’s own services (t.Where<ITenantContext>((tenant, _) => o => o.TenantId == tenant.TenantId)), failing closed if the filter throws or the service won’t resolve. Learn more - MCP server —
Shiny.DocumentDb.Mcpplus theShinyDocDbMcpdotnet tool point Claude Code, Claude Desktop, Copilot, or any MCP client at a store. The tools are the sameExtensions.AItools — one implementation, one security model — plus resources (documentdb://types,.../schema,.../sample,documentdb://stats), two prompts, and an audit line per call. Read-only by default; writes need two locks (the per-type capability andAllowWrites()), and there is no raw-SQL tool and no schema mutation. The stdio tool discovers what to expose from the storedTypeNamediscriminators, so it needs no compiled document classes. Learn more - REST & live-query endpoints —
Shiny.DocumentDb.AspNetCoreturns a document type into a complete HTTP resource in one line:app.MapDocuments<Order>("/orders", …)gives list, by-id, count, create, replace, RFC 7396 merge-patch, delete, and a live Server-Sent-Events tail. Filtering runs the store’s own string grammar behind a per-endpoint field allowlist (an unlisted field is a400, not a table scan),takeis clamped, and cursor paging, sparse fieldsets,ETag/If-Matchconcurrency andProblemDetailserrors are all in.Scope(...)is the HTTP twin of the AI tools’ non-removable filter, resolved per request. Plain JSON, framework reference only, AOT-clean. Learn more - VectorData connector —
Shiny.DocumentDb.Extensions.VectorDatapoints the .NET AI ecosystem (MEAI, the Microsoft Agent Framework, Semantic Kernel) at a document store throughMicrosoft.Extensions.VectorData’sVectorStore/VectorStoreCollection<TKey, TRecord>. Every other MEVD connector is single-store; this one runs the same record model over any vector-capable DocumentDb backend — SQLite for dev and mobile, PostgreSQL/pgvector or SQL Server for production, Cosmos / Atlas / Redis — swapped by configuration. Learn more - Orleans persistence —
Shiny.DocumentDb.Orleansprovides a full Microsoft Orleans stack — grain storage, reminders, cluster membership, and grain directory — on any DocumentDb backend (relational, MongoDB, or Cosmos) through oneIDocumentStoreabstraction. Because grain state is persisted as structured, queryable JSON, you can query grain state directly without activating grains (reporting, dashboards, ops tooling) and get a free audit trail of every mutation viacfg.MapTemporal(). Learn more - Telemetry & diagnostics — embedded and always-on: every store emits OpenTelemetry-native metrics (
db.client.operation.durationand friends, plus adb.client.unit_of_work.operationshistogram) andActivitySourcetrace spans per operation — CRUD, fluent-query terminals, temporal, and aunit_of_workparent span per session. Built onSystem.Diagnostics; zero-cost when nobody is listening — subscribe with.AddSource/.AddMeter("Shiny.DocumentDb"). Learn more - JSON Schema validation —
Shiny.DocumentDb.JsonSchemaattaches a JSON Schema (draft 2020-12) to a document type and validates the exact JSON about to be persisted just before the write.options.ConfigureDocument<Customer>(cfg => cfg.MapJsonSchema(schemaJson))needs no DI (works with a hand-builtnew DocumentStore(options)); a failure throwsDocumentSchemaValidationExceptionwith field-level errors and rolls the write back. Enforces what the C# type can’t —maxLength, ranges,pattern,enum,format. Learn more - OData query endpoints —
Shiny.DocumentDb.OData+Shiny.DocumentDb.AspNetCore.ODataexpose a document type as an OData v4 entity set:$filter/$orderby/$top/$skip/$count/$selecttranslate onto the fluent query and run against any provider. Global query filters always apply underneath, and per-entity-setODataQueryPolicygovernance locks down public endpoints. Learn more - Offline-first sync —
Shiny.DocumentDb.AppDataSyncmakes the store the local cache of an offline-first app that bidirectionally syncs to an HTTP backend viaShiny.Data.Sync.SyncDocumentStore(sync => sync.Sync<TodoItem>())turns an ordinary document type into a two-way synced one — every local write is auto-enqueued to the outbox and every pulled server change is auto-applied back. Client-tier providers (SQLite, LiteDB, IndexedDB). Learn more - .NET Aspire integration —
Shiny.DocumentDb.Aspire.Hosting/.Client/.Orleansmake the backend a deployment decision:builder.AddPostgresDocumentStore("orders").WithSeeder(...)in the AppHost picks the provider and gates seeding; the consuming service callsbuilder.AddDocumentStore("orders")for the keyed store wired with health checks + OpenTelemetry.builder.AddDocumentDbAdmin()brings the admin UI up alongside them, already connected to every referenced store. Learn more - Admin UI — ShinyDocDbMyAdmin, a phpMyAdmin-style front end in two shapes over one core: a web app shipped as a container image (
ghcr.io/shinyorg/shiny-docdb-myadmin) and a terminal UI installable as a dotnet tool (dotnet tool install -g ShinyDocDbMyAdmin.Tui, thenshinydocdb). Browse documents in a sampled-column grid with JSON-path filters, edit them as JSON, inspect the inferred structure and create/drop indexes in one click, runEXPLAIN, diff and restore temporal versions, map GeoJSON, search full text, inspect vectors, preview blobs, watch the outbox queue, generate shape-matched test data, stream import/export (JSON, NDJSON, CSV, envelope), and ask a read-only AI assistant about your data. Encryption-aware throughout: envelopes are recognized without a key, key coverage is reported, and a write that would silently downgrade ciphertext to plaintext is blocked. Relational providers. Learn more
-
Install the NuGet packages
Install the core package plus your provider:
Terminal window dotnet add package Shiny.DocumentDb.SqliteTerminal window dotnet add package Shiny.DocumentDb.Sqlite.SqlCipherTerminal window dotnet add package Shiny.DocumentDb.SqlServerTerminal window dotnet add package Shiny.DocumentDb.MySqlTerminal window dotnet add package Shiny.DocumentDb.MariaDbTerminal window dotnet add package Shiny.DocumentDb.PostgreSqlTerminal window dotnet add package Shiny.DocumentDb.CockroachDbTerminal window dotnet add package Shiny.DocumentDb.OracleTerminal window dotnet add package Shiny.DocumentDb.MongoDbTerminal window dotnet add package Shiny.DocumentDb.DuckDbTerminal window dotnet add package Shiny.DocumentDb.IndexedDbCosmos DB, Azure Table, DynamoDB, Amazon DocumentDB, Redis, RavenDB, Firestore, and LiteDB each have their own package too — see the provider reference for the full list and what each one supports.
Each provider package includes the core
Shiny.DocumentDbpackage automatically — which now bundles the dependency-injection registration (AddDocumentStore,AddDocumentContext, seeding), field-level encryption, the transactional outbox, and OpenTelemetry instrumentation, so there’s no separate package to install. -
Register with dependency injection:
using Shiny.DocumentDb;services.AddDocumentStore(opts =>{opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db");});Just swap the provider for your database:
opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db");opts.DatabaseProvider = new SqlCipherDatabaseProvider("mydata.db", "mySecretKey");opts.DatabaseProvider = new SqlServerDatabaseProvider("Server=localhost;Database=mydb;Trusted_Connection=true;");opts.DatabaseProvider = new MySqlDatabaseProvider("Server=localhost;Database=mydb;User=root;Password=pass;");opts.DatabaseProvider = new MariaDbDatabaseProvider("Server=localhost;Database=mydb;User=root;Password=pass;");opts.DatabaseProvider = new PostgreSqlDatabaseProvider("Host=localhost;Database=mydb;Username=postgres;Password=pass;");opts.DatabaseProvider = new CockroachDbDatabaseProvider("Host=localhost;Port=26257;Username=root;Database=defaultdb;SSL Mode=Disable;");opts.DatabaseProvider = new OracleDatabaseProvider("User Id=myuser;Password=pass;Data Source=localhost:1521/FREEPDB1");opts.DatabaseProvider = new DuckDbDatabaseProvider("Data Source=mydata.duckdb");The document-native and key-partitioned providers — MongoDB, Amazon DocumentDB, Cosmos DB, Azure Table, DynamoDB, Redis, RavenDB, Firestore, LiteDB, and IndexedDB — each use their own options class, so register the store directly with the DI container:
builder.Services.AddSingleton(new MongoDbDocumentStoreOptions{ConnectionString = "mongodb://localhost:27017",DatabaseName = "mydb"});builder.Services.AddSingleton<IDocumentStore, MongoDbDocumentStore>();Their provider pages carry the exact options for each.
For multiple databases, register named stores using .NET keyed services:
services.AddDocumentStore("users", opts =>{opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=users.db");});services.AddDocumentStore("analytics", opts =>{opts.DatabaseProvider = new PostgreSqlDatabaseProvider("Host=...");});Inject via
[FromKeyedServices("name")]or resolve dynamically withIDocumentSessionFactory:public class MyService([FromKeyedServices("users")] IDocumentStore userStore,[FromKeyedServices("analytics")] IDocumentStore analyticsStore) { }// Or dynamically:public class MyService(IDocumentSessionFactory stores){void DoWork() => stores.GetStore("users").Insert(...);}For multi-tenant applications, two isolation strategies are available:
// Shared-table: single database, automatic TenantId column filteringservices.AddSingleton<ITenantResolver, MyTenantResolver>();services.AddDocumentStore(opts =>{opts.DatabaseProvider = new PostgreSqlDatabaseProvider("Host=...");}, multiTenant: true);// ...or a named/keyed shared-table store (resolve with [FromKeyedServices("orders")]):services.AddDocumentStore("orders", opts =>{opts.DatabaseProvider = new PostgreSqlDatabaseProvider("Host=...");}, multiTenant: true);// Tenant-per-database: separate database per tenant (scoped IDocumentStore, bounded cache)services.AddSingleton<ITenantResolver, MyTenantResolver>();services.AddMultiTenantDocumentStore(tenantId => new DocumentStoreOptions{DatabaseProvider = new SqliteDatabaseProvider($"Data Source={tenantId}.db")});See Multi-Tenancy for choosing between them, the store-cache knobs, per-tenant seeding, and
ITenantStoreManager.Both require an
ITenantResolverimplementation:public class MyTenantResolver(IHttpContextAccessor http) : ITenantResolver{public string GetCurrentTenant()=> http.HttpContext?.User.FindFirst("tenant_id")?.Value?? throw new InvalidOperationException("No tenant context");}Or instantiate directly (no DI needed):
// Quick setup (SQLite convenience class)var store = new SqliteDocumentStore("Data Source=mydata.db");// Full optionsvar store = new SqliteDocumentStore(new DocumentStoreOptions{DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")});// Quick setupvar store = new SqlCipherDocumentStore("mydata.db", "mySecretKey");// Full optionsvar store = new SqlCipherDocumentStore(new DocumentStoreOptions{DatabaseProvider = new SqlCipherDatabaseProvider("mydata.db", "mySecretKey")});var store = new DocumentStore(new DocumentStoreOptions{DatabaseProvider = new SqlServerDatabaseProvider("Server=localhost;Database=mydb;Trusted_Connection=true;")});var store = new DocumentStore(new DocumentStoreOptions{DatabaseProvider = new MySqlDatabaseProvider("Server=localhost;Database=mydb;User=root;Password=pass;")});var store = new DocumentStore(new DocumentStoreOptions{DatabaseProvider = new PostgreSqlDatabaseProvider("Host=localhost;Database=mydb;Username=postgres;Password=pass;")});var store = new DocumentStore(new DocumentStoreOptions{DatabaseProvider = new DuckDbDatabaseProvider("Data Source=mydata.duckdb")});var store = new MongoDbDocumentStore(new MongoDbDocumentStoreOptions{ConnectionString = "mongodb://localhost:27017",DatabaseName = "mydb"}); -
Inject
IDocumentStoreand start using it:public class MyService(IDocumentStore store){public async Task SaveUser(User user){await store.Insert(user); // Id auto-generated for Guid/int/long; string Ids must be set}public async Task<User?> GetUser(string id){return await store.Get<User>(id);}public async Task<IReadOnlyList<User>> GetActiveUsers(){return await store.Query<User>().Where(u => u.IsActive).OrderBy(u => u.Name).ToList();}}
Configuration Options
Section titled “Configuration Options”| Property | Type | Default | Description |
|---|---|---|---|
DatabaseProvider |
IDatabaseProvider (required) |
— | The database provider to use (e.g. SqliteDatabaseProvider, SqlCipherDatabaseProvider, SqlServerDatabaseProvider, MySqlDatabaseProvider, MariaDbDatabaseProvider, PostgreSqlDatabaseProvider, CockroachDbDatabaseProvider, OracleDatabaseProvider, DuckDbDatabaseProvider). The document-native and key-partitioned stores use their own options classes. |
TableName |
string |
"documents" |
Default table name for all document types that do not set cfg.Table |
TypeNameResolution |
TypeNameResolution |
ShortName |
How type names are stored (ShortName or FullName) |
JsonSerializerOptions |
JsonSerializerOptions? |
null |
JSON serialization settings. When a JsonSerializerContext is attached as the TypeInfoResolver, all methods auto-resolve type info from the context |
UseReflectionFallback |
bool |
true |
When false, throws InvalidOperationException if a type can’t be resolved from the configured TypeInfoResolver instead of falling back to reflection. Recommended for AOT deployments |
Logging |
Action<string>? |
null |
Callback invoked with every SQL statement executed |
SkipTableInitialization |
bool |
false |
Skips lazy table/index creation. For pointing at a database you don’t own (an admin tool, a read-only replica) |
TenantIdAccessor |
Func<string>? |
null |
When set, enables shared-table multi-tenancy. All queries are filtered by TenantId and all inserts include the TenantId value. A dedicated TenantId column and index are created automatically |
Field-level encryption is configured on the same options object with opts.UseEncryptor(...), and the
transactional outbox with opts.AddOutbox().
Per-Type Configuration
Section titled “Per-Type Configuration”Everything about a document type is configured in one ConfigureDocument<T> block, with the type named once.
By default all types share a single table; set cfg.Table to give one its own. Tables are lazily created on
first use.
var options = new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db"), TableName = "docs" // change the default table name (optional)};
options.ConfigureDocument<Order>(cfg => cfg.Table = "orders"); // explicit table nameoptions.ConfigureDocument<AuditLog>(cfg => cfg.Table = cfg.TypeName); // named after the type// User stays in the default "docs" table
var store = new DocumentStore(options);Custom Id property
Section titled “Custom Id property”By default every document type must have a property named Id. Override it with cfg.MapIdProperty(...) —
on its own, or alongside cfg.Table. The two are independent.
options.ConfigureDocument<Sensor>(cfg =>{ cfg.Table = "sensors"; cfg.MapIdProperty(s => s.DeviceKey); // Guid DeviceKey as Id, in its own table});
options.ConfigureDocument<BlogPost>(cfg => cfg.MapIdProperty(p => p.Slug)); // default shared tableThe builder surface
Section titled “The builder surface”| Member | Description |
|---|---|
cfg.Table |
The type’s storage unit — a table, or a collection / container / object store on the document providers. Leave it unset to share the store’s default |
cfg.TypeName |
The resolved type name, per the store’s TypeNameResolution — assign it to cfg.Table to name the table after the type |
cfg.MapIdProperty(x => x.MyProp) |
Custom Id property. A string overload is the AOT-safe form |
cfg.MapVersionProperty(x => x.RowVersion) |
Optimistic concurrency |
cfg.AddQueryFilter(...) |
A global query filter, optionally named |
cfg.MapProperty(x => x.Ssn, p => p.Encrypt(...)) |
Per-property options — field-level encryption today |
cfg.MapJsonSchema(schemaJson) |
JSON Schema (draft 2020-12) validated just before the write |
cfg.MapSpatialProperty / cfg.MapVectorProperty / cfg.MapFullTextProperty |
Search mappings — one of each per type |
cfg.MapComputedProperty<TValue>(...) |
A derived value you can filter, sort and project by |
cfg.MapBlob / cfg.MapBlobCollection |
Sidecar blob payloads |
cfg.MapTemporal(...) |
Append-only system-time history |
cfg.OnBeforeWrite / cfg.OnAfterWrite |
Write hooks scoped to this type |
cfg.AddSoftDelete(x => x.IsDeleted) |
Soft delete |
cfg.PublishToOutbox(ctx => new OrderChanged(...)) |
Enqueue an outbox message on every write of this type |
Provider packages add their own vocabulary and features to the same builder — cfg.ToContainer(...) on Cosmos,
cfg.ToCollection(...) on MongoDB / LiteDB / Firestore, cfg.ToStore(...) on IndexedDB,
cfg.ToPartition(...) and cfg.MapIndexedProperty(...) on Azure Table / DynamoDB, cfg.MapIndexedProperty(...)
on Redis. Calling ConfigureDocument<T> more than once for the same type is additive.
Mapping a feature the chosen backend does not have is reported when the store is built —
DocumentConfigurationException lists every problem at once rather than one restart at a time.
DI Registration with Table Mapping
Section titled “DI Registration with Table Mapping”services.AddDocumentStore(opts =>{ opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db"); opts.ConfigureDocument<User>(cfg => cfg.Table = cfg.TypeName); opts.ConfigureDocument<Order>(cfg => cfg.Table = "orders"); opts.ConfigureDocument<Sensor>(cfg => { cfg.Table = "sensors"; cfg.MapIdProperty(s => s.DeviceKey); });});AI Coding Assistant
Section titled “AI Coding Assistant”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.


