Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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.

Frameworks
.NET
.NET MAUI
Blazor
ASP.NET
Operating Systems
Android
iOS
Windows
GitHub GitHub stars for shinyorg/DocumentDb
Core (Shiny.DocumentDb) NuGet downloads for Shiny.DocumentDb

Install one — it brings the core package with it. Provider reference

SQLite NuGet downloads for Shiny.DocumentDb.Sqlite
SQLite vector support (sqlite-vec) NuGet downloads for Shiny.DocumentDb.Sqlite.VectorSupport
SQLCipher (encrypted SQLite) NuGet downloads for Shiny.DocumentDb.Sqlite.SqlCipher
PostgreSQL NuGet downloads for Shiny.DocumentDb.PostgreSql
CockroachDB NuGet downloads for Shiny.DocumentDb.CockroachDb
SQL Server NuGet downloads for Shiny.DocumentDb.SqlServer
MySQL NuGet downloads for Shiny.DocumentDb.MySql
MariaDB NuGet downloads for Shiny.DocumentDb.MariaDb
Oracle NuGet downloads for Shiny.DocumentDb.Oracle
DuckDB NuGet downloads for Shiny.DocumentDb.DuckDb
Azure Cosmos DB NuGet downloads for Shiny.DocumentDb.CosmosDb
Azure Table Storage NuGet downloads for Shiny.DocumentDb.AzureTable
Amazon DynamoDB NuGet downloads for Shiny.DocumentDb.DynamoDb
MongoDB NuGet downloads for Shiny.DocumentDb.MongoDb
Amazon DocumentDB NuGet downloads for Shiny.DocumentDb.DocumentDb
Redis NuGet downloads for Shiny.DocumentDb.Redis
RavenDB NuGet downloads for Shiny.DocumentDb.RavenDb
Google Firestore NuGet downloads for Shiny.DocumentDb.Firestore
LiteDB NuGet downloads for Shiny.DocumentDb.LiteDb
IndexedDB (Blazor WASM) NuGet downloads for Shiny.DocumentDb.IndexedDb
AI tools (Microsoft.Extensions.AI) NuGet downloads for Shiny.DocumentDb.Extensions.AI
MCP server NuGet downloads for Shiny.DocumentDb.Mcp
VectorData connector (MEVD) NuGet downloads for Shiny.DocumentDb.Extensions.VectorData
REST & live queries (ASP.NET Core) NuGet downloads for Shiny.DocumentDb.AspNetCore
OData NuGet downloads for Shiny.DocumentDb.OData
OData (ASP.NET Core host) NuGet downloads for Shiny.DocumentDb.AspNetCore.OData
JSON Schema validation NuGet downloads for Shiny.DocumentDb.JsonSchema
Reference geo data NuGet downloads for Shiny.DocumentDb.Geo
Offline sync (App Data Sync) NuGet downloads for Shiny.DocumentDb.AppDataSync
Orleans persistence NuGet downloads for Shiny.DocumentDb.Orleans
Aspire (AppHost) NuGet downloads for Shiny.DocumentDb.Aspire.Hosting
Aspire (Client) NuGet downloads for Shiny.DocumentDb.Aspire.Client
Aspire (Orleans) NuGet downloads for Shiny.DocumentDb.Aspire.Orleans
MCP server (dotnet tool) shiny-documentdb-mcp NuGet downloads for ShinyDocDbMcp
Admin terminal UI (dotnet tool) shinydocdb NuGet downloads for ShinyDocDbMyAdmin.Tui

The web admin UI ships as a container image — ghcr.io/shinyorg/shiny-docdb-myadmin. Admin UI

  • 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 builderstore.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 via Sql.* markers; collection-level Sum, Min, Max, Average on child collections
  • Grouped aggregationQuery<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 SQL GROUP BY/HAVING on 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 SQL LIMIT/OFFSET
  • Single-row terminalsFirst, FirstOrDefault, Single, and SingleOrDefault end a query with one document, with predicate (First(x => x.Age == 40)) and string-grammar (First("status == 'open'")) overloads. Not sugar over ToList() — the row limit reaches the provider, so a relational store emits LIMIT 1 and MongoDB/Cosmos page server-side. Every provider. Learn more
  • Raw JSON results — end a typed Query<T>() with JSON instead of T, so a document that only has to reach an HTTP response never becomes an object: FirstOrDefaultRawJson(), WriteJsonArrayTo(stream) (streamed, never buffered), plus a JsonObject lane (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 mappingcfg.Table = "orders" inside a ConfigureDocument<T> block gives a document type its own dedicated table. Unmapped types share a configurable default table
  • Custom Id propertiescfg.MapIdProperty(x => x.MyProp) overrides the Id, on its own or alongside cfg.Table
  • Document diffingGetDiff compares a modified object against the stored document and returns an RFC 6902 JsonPatchDocument<T> with deep nested-object diffing
  • Surgical field updatesSetProperty updates a single JSON field without deserialization. RemoveProperty strips a field. Both support nested paths
  • JSON Merge Patch (Upsert)Upsert uses RFC 7396 json_patch to 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, and Upsert(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 throw NotSupportedException on the document-native and key-partitioned stores. Learn more
  • Bulk operationsQuery<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 lookupsGet, Remove, SetProperty, and RemoveProperty accept the Id as object so you can pass a Guid, int, long, or string directly. Unsupported types throw ArgumentException
  • 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 give Insert/Update/Upsert/Get/Remove/Clear plus a fluent string-grammar Query() returning JsonObjects. 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 explicit total:number hint 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 over IDocumentStore. Declare aggregates on a partial context with [Document(typeof(User), Id = nameof(User.Email), JsonContext = typeof(AppJsonContext))] and a bundled source generator emits a DocumentSet<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() — with JsonTypeInfo<T> threaded automatically. A static partial void OnConfiguring(DocumentModelBuilder model) hook lets the context declare its whole document model next to its [Document] list instead of inside AddDocumentStore. Ships in core, works over all providers. Learn more
  • Computed properties — map a value derived from other fields (Total = Quantity * UnitPrice, a normalized lower(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); pass indexed: true on 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 configured JsonSerializerContext. Set UseReflectionFallback = false to catch missing registrations with clear exceptions
  • Validate-on-build — one configuration sweep when the store is constructed, reporting every problem together through DocumentConfigurationException instead of one per restart. It catches features the chosen backend doesn’t have (a vector mapping on LiteDB, cfg.Table on RavenDB) and randomized-encrypted properties used where the database has to read through them. DocumentConfigurationValidator.Collect(options) returns the same list without throwing
  • Optimistic concurrencycfg.MapVersionProperty(x => x.RowVersion) enables automatic version checking on update/upsert. Version is set to 1 on insert, checked and incremented on update. Throws ConcurrencyException on conflict. Works across all providers — stored in the JSON blob with zero schema changes
  • Unit of workstore.OpenSession() + SaveChanges() with automatic commit/rollback. IDocumentSession is the scoped, per-request front end (AddScopedDocumentSession()); IDocumentSessionFactory opens one where there’s no ambient DI scope (MAUI/desktop, workers, Orleans grains, seeders) and resolves named stores with GetStore("name")
  • Batch writesBatchInsert inserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure. BatchUpsert, BatchUpdate, and BatchRemove<T>(ids) apply many writes as one set operation (a single multi-row INSERT … ON CONFLICT deep-merge on SQLite/DuckDB, one BulkWrite/DeleteMany on MongoDB, parallel request waves on Cosmos, a single DELETE … 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 full Geometry model (GeoLineString, GeoPolygon with holes, multi-geometries) maps via cfg.MapSpatialProperty(x => x.Area) and queries with the topological predicate family — GeoIntersects, GeoContains, GeoWithin, GeoCovers, GeoWithinDistance, and friends — returning SpatialResult<T> with DistanceMeters. Compose spatial predicates inside ordinary LINQ with DocumentFunctions.Intersects/Distance/… (server-side, combinable with other Where/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, MySQL SPATIAL, DuckDB R-Tree, SQL Server spatial index, Oracle SDO_GEOMETRY + MDSYS — plus native ST_* on CosmosDB and 2dsphere on MongoDB. Learn more
  • Vector / ANN search — register an embedding property with cfg.MapVectorProperty(d => d.Embedding, dimensions: 1536, metric: VectorDistance.Cosine) and query with Query<T>().Where(...).NearestVectors(query, k). Provider-native indexes: pgvector (PostgreSQL), VECTOR + DiskANN (SQL Server 2025), native VECTOR + HNSW/IVF (Oracle 23ai), embedding policy (CosmosDB), $vectorSearch (MongoDB Atlas), vss extension (DuckDB), sqlite-vec (SQLite). Plus AutoEmbedOnInsert<T> to plug in Microsoft.Extensions.AI.IEmbeddingGenerator and 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, returning FullTextResult<T> (Document + normalized Score) ordered by relevance, with an optional pre-filter and a fluent store.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, the fts extension (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 indexesCreateIndexAsync(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 backupBackup copies the database to a file. Available on SqliteDocumentStore, SqlCipherDocumentStore, and LiteDbDocumentStore
  • Streaming bulk export / import / restore — the IDocumentBackup store capability moves a whole store’s contents in and out as a portable, streamed v1 backup: ExportAsync(Stream), RestoreAsync(Stream), and the lower-level BulkImportAsync(IAsyncEnumerable<RawDocument>). Bodies bound verbatim (no <T>, no reflection — AOT-friendly); BulkWriteMode picks Insert/Replace/Merge/SkipExisting, with a native bulk-copy fast path (10-100× faster) on PostgreSQL COPY, SQL Server SqlBulkCopy, 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 relational DocumentStore (SQLite, SQL Server, PostgreSQL, MySQL, DuckDB, Oracle), MongoDB, and CosmosDB; the older SqliteDocumentStore.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 a DocumentSeedMarker (bump Version to re-run). Wire with AddDocumentSeeder<T>() / AddDocumentSeeder(name, version, delegate) at host startup, or call DocumentSeedRunner.RunAsync(store, seeders) directly (e.g. on MAUI)
  • SQLCipher encryption — separate Shiny.DocumentDb.Sqlite.SqlCipher package with AES-256 encryption, password-aware backup, and RekeyAsync to change the encryption key
  • Field-level encryption — in the core package, on every provider. opts.UseEncryptor(new AesGcmDocumentEncryptor("k1", key)) plus cfg.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 a JsonTypeInfo modifier, so every write path — temporal history, backup export — is covered by construction and no provider needs to know about it. EncryptionMode.Deterministic keeps 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 plus RewrapAsync<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 TenantId column 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-implemented ITenantResolver. Consumer code is unchanged — tenant isolation is applied transparently
  • Change monitoring — consume an IAsyncEnumerable<DocumentChange<T>> of insert/update/remove/clear events with await foreach (var c in store.NotifyOnChange<User>(ct)). Filter to a single document with WhenDocumentChanged<T>(id) or to the result set of a fluent query with query.NotifyOnChange(). Buffered in a session and emitted on commit. Learn more
  • Native change feedsIChangeFeedDocumentStore.SubscribeChanges<T> observes all writers via the database’s own mechanism: PostgreSQL LISTEN/NOTIFY triggers, SQL Server Change Tracking (optionally with SqlDependency query 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 with History<T>(id), AsOf<T>(id, when), Restore<T>(id, version), GetDiffBetween<T>(id, from, to), plus fleet-wide AsOfAll<T>(when), ChangesByActor<T>(actor), and ChangesBetween<T>(from, to) — on the ITemporalDocumentStore capability interface, not IDocumentStore. Opt-in per type, on every provider (relational and document/NoSQL). Learn more
  • Write interceptors — observe and mutate writes as they happen, per-document (IDocumentInterceptorInsert, BatchInsert per item, Update, Upsert, Remove) or set-based (IDocumentBulkInterceptorExecuteUpdate, 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 also ctx.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>(), then session.Add(order).Enqueue(new OrderPlaced(...)) — both rows commit together or neither does. Or declaratively per type with cfg.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, IOutboxAdmin is the operational view, and the traceparent captured 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 via IDocumentStore.SupportsTransactions. Learn more
  • Soft deletecfg.AddSoftDelete(x => x.IsDeleted) and Remove, ExecuteDelete, and Clear all 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 raw byte[], which base64-inflates the Data column ~33% and materializes on every read. Learn more
  • Reference geo dataShiny.DocumentDb.Geo ships 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 of T, plus Get/Update/Remove/SetProperty/RemoveProperty/Clear/ExecuteUpdate/ExecuteDelete and per-query change monitoring. Mirrors Entity Framework Core’s HasQueryFilter, including named filters, IgnoreQueryFilters()/IgnoreQueryFilters("name"), and captured-variable semantics. Learn more
  • AI tool integrationShiny.DocumentDb.Extensions.AI exposes document types as Microsoft.Extensions.AI tool functions for LLM agents. Per-type capability flags (ReadOnly, All), structured filter expressions, field visibility control, and page size caps. A non-removable per-type Where scope 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 serverShiny.DocumentDb.Mcp plus the ShinyDocDbMcp dotnet tool point Claude Code, Claude Desktop, Copilot, or any MCP client at a store. The tools are the same Extensions.AI tools — 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 and AllowWrites()), and there is no raw-SQL tool and no schema mutation. The stdio tool discovers what to expose from the stored TypeName discriminators, so it needs no compiled document classes. Learn more
  • REST & live-query endpointsShiny.DocumentDb.AspNetCore turns 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 a 400, not a table scan), take is clamped, and cursor paging, sparse fieldsets, ETag/If-Match concurrency and ProblemDetails errors 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 connectorShiny.DocumentDb.Extensions.VectorData points the .NET AI ecosystem (MEAI, the Microsoft Agent Framework, Semantic Kernel) at a document store through Microsoft.Extensions.VectorData’s VectorStore / 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 persistenceShiny.DocumentDb.Orleans provides a full Microsoft Orleans stack — grain storage, reminders, cluster membership, and grain directory — on any DocumentDb backend (relational, MongoDB, or Cosmos) through one IDocumentStore abstraction. 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 via cfg.MapTemporal(). Learn more
  • Telemetry & diagnostics — embedded and always-on: every store emits OpenTelemetry-native metrics (db.client.operation.duration and friends, plus a db.client.unit_of_work.operations histogram) and ActivitySource trace spans per operation — CRUD, fluent-query terminals, temporal, and a unit_of_work parent span per session. Built on System.Diagnostics; zero-cost when nobody is listening — subscribe with .AddSource/.AddMeter("Shiny.DocumentDb"). Learn more
  • JSON Schema validationShiny.DocumentDb.JsonSchema attaches 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-built new DocumentStore(options)); a failure throws DocumentSchemaValidationException with field-level errors and rolls the write back. Enforces what the C# type can’t — maxLength, ranges, pattern, enum, format. Learn more
  • OData query endpointsShiny.DocumentDb.OData + Shiny.DocumentDb.AspNetCore.OData expose a document type as an OData v4 entity set: $filter/$orderby/$top/$skip/$count/$select translate onto the fluent query and run against any provider. Global query filters always apply underneath, and per-entity-set ODataQueryPolicy governance locks down public endpoints. Learn more
  • Offline-first syncShiny.DocumentDb.AppDataSync makes the store the local cache of an offline-first app that bidirectionally syncs to an HTTP backend via Shiny.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 integrationShiny.DocumentDb.Aspire.Hosting / .Client / .Orleans make the backend a deployment decision: builder.AddPostgresDocumentStore("orders").WithSeeder(...) in the AppHost picks the provider and gates seeding; the consuming service calls builder.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 UIShinyDocDbMyAdmin, 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, then shinydocdb). 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, run EXPLAIN, 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
  1. Install the NuGet packages

    Install the core package plus your provider:

    Terminal window
    dotnet add package Shiny.DocumentDb.Sqlite

    Cosmos 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.DocumentDb package 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.

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

    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 with IDocumentSessionFactory:

    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 filtering
    services.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 ITenantResolver implementation:

    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 options
    var store = new SqliteDocumentStore(new DocumentStoreOptions
    {
    DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")
    });
  3. Inject IDocumentStore and 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();
    }
    }
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().

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 name
options.ConfigureDocument<AuditLog>(cfg => cfg.Table = cfg.TypeName); // named after the type
// User stays in the default "docs" table
var store = new DocumentStore(options);

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 table
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.

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);
});
});
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One 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.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One 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.

View shiny-documentdb Skill