VOOZH about

URL: https://shinylib.net/documentdb/

โ‡ฑ Document DB | Shiny.NET


Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

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
Downloads๐Ÿ‘ NuGet downloads for Shiny.DocumentDb
SQLite๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Sqlite
SQLCipher๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Sqlite.SqlCipher
SQL Server๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.SqlServer
MySQL๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.MySql
PostgreSQL๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.PostgreSql
Oracle๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Oracle
LiteDB๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.LiteDb
CosmosDB๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.CosmosDb
MongoDB๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.MongoDb
DuckDB๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.DuckDb
DI Extensions๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Extensions.DependencyInjection
IndexedDB๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.IndexedDb
AI Tools๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Extensions.AI
Orleans๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.Orleans
JSON Schema Validation๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.JsonSchema
Offline Sync (App Data Sync)๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.AppDataSync
OData๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.OData
OData (ASP.NET Core host)๐Ÿ‘ NuGet downloads for Shiny.DocumentDb.AspNetCore.OData
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
  • Multi-provider โ€” SQLite, SQLCipher (encrypted SQLite), LiteDB, CosmosDB, MongoDB, DuckDB, IndexedDB (Blazor WASM), SQL Server, MySQL, PostgreSQL, and Oracle with a single API
  • 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 via Sql.* markers; collection-level Sum, Min, Max, Average on child collections
  • Ordering โ€” .OrderBy(u => u.Age) and .OrderByDescending(u => u.Name) on the fluent query builder
  • Pagination โ€” .Paginate(offset, take) translates to SQL LIMIT/OFFSET
  • Table-per-type mapping โ€” MapTypeToTable<T>() gives a document type its own dedicated table. Unmapped types share a configurable default table
  • Custom Id properties โ€” MapTypeToTable<T>("table", x => x.MyProp) to combine with a dedicated table, or MapIdProperty<T>(x => x.MyProp) to override the Id while keeping the type in the default shared table
  • Document diffing โ€” GetDiff compares a modified object against the stored document and returns an RFC 6902 JsonPatchDocument<T> with deep nested-object diffing
  • Surgical field updates โ€” SetProperty 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
  • 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
  • Typed Id lookups โ€” Get, Remove, SetProperty, and RemoveProperty accept the Id as object so you can pass a Guid, int, long, or string directly. Unsupported types throw ArgumentException
  • 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
  • Optimistic concurrency โ€” MapVersionProperty<T>(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 work โ€” CreateUnitOfWork() + SaveChanges() with automatic commit/rollback
  • Batch writes โ€” BatchInsert 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 โ€” WithinRadius, WithinBoundingBox, and NearestNeighbors with GeoPoint support. SQLite uses R*Tree; CosmosDB uses native ST_DISTANCE/ST_WITHIN. Learn more
  • Vector / ANN search โ€” register an embedding property with MapVectorProperty<T>(d => d.Embedding, dimensions: 1536, metric: VectorDistance.Cosine, indexKind: VectorIndexKind.Hnsw) 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) โ€” MapFullTextProperty<T>(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 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 โ€” Backup copies the database to a file. Available on SqliteDocumentStore, SqlCipherDocumentStore, and LiteDbDocumentStore
  • 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
  • Multi-tenancy โ€” two isolation strategies: shared-table (single database with automatic TenantId column filtering) and tenant-per-database (separate database per tenant via lazy factory). 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 UnitOfWork and emitted on commit. Learn more
  • Native change feeds โ€” IChangeFeedDocumentStore.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) โ€” MapTemporal<T>(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
  • Global query filters โ€” register an AddQueryFilter<T>(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 integration โ€” Shiny.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. Learn more
  • Orleans persistence โ€” Shiny.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 MapTemporal<T>. Learn more
  • Telemetry & diagnostics โ€” Shiny.DocumentDb.Diagnostics wraps any provider with AddDocumentStoreInstrumentation() to emit OpenTelemetry-native metrics (db.client.operation.duration and friends) and ActivitySource trace spans per operation โ€” CRUD, fluent-query terminals, temporal, and transactions (as parent spans). Built on System.Diagnostics.Metrics; zero-cost when nobody is listening. Learn more
  • JSON Schema validation โ€” Shiny.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.MapJsonSchema<Customer>(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 endpoints โ€” Shiny.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 sync โ€” Shiny.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 integration โ€” Shiny.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. Learn more
  1. Install the NuGet packages

    Install the core package plus your provider:

    Each provider package includes the core Shiny.DocumentDb package automatically.

    For dependency injection, also install the DI extensions package:

    Terminal window
    dotnetaddpackageShiny.DocumentDb.Extensions.DependencyInjection
  2. Register with dependency injection:

    usingShiny.DocumentDb;
    services.AddDocumentStore(opts=>
    {
    opts.DatabaseProvider =newSqliteDatabaseProvider("Data Source=mydata.db");
    });

    Just swap the provider for your database:

    MongoDB uses its own options class โ€” register the store directly with the DI container:

    builder.Services.AddSingleton(newMongoDbDocumentStoreOptions
    {
    ConnectionString ="mongodb://localhost:27017",
    DatabaseName ="mydb"
    });
    builder.Services.AddSingleton<IDocumentStore, MongoDbDocumentStore>();

    For multiple databases, register named stores using .NET keyed services:

    services.AddDocumentStore("users", opts=>
    {
    opts.DatabaseProvider =newSqliteDatabaseProvider("Data Source=users.db");
    });
    services.AddDocumentStore("analytics", opts=>
    {
    opts.DatabaseProvider =newPostgreSqlDatabaseProvider("Host=...");
    });

    Inject via [FromKeyedServices("name")] or resolve dynamically with IDocumentStoreProvider:

    publicclassMyService(
    [FromKeyedServices("users")] IDocumentStoreuserStore,
    [FromKeyedServices("analytics")] IDocumentStoreanalyticsStore) { }
    // Or dynamically:
    publicclassMyService(IDocumentStoreProviderstores)
    {
    voidDoWork() => 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 =newPostgreSqlDatabaseProvider("Host=...");
    }, multiTenant: true);
    // ...or a named/keyed shared-table store (resolve with [FromKeyedServices("orders")]):
    services.AddDocumentStore("orders", opts=>
    {
    opts.DatabaseProvider =newPostgreSqlDatabaseProvider("Host=...");
    }, multiTenant: true);
    // Tenant-per-database: separate database per tenant (scoped IDocumentStore)
    services.AddSingleton<ITenantResolver, MyTenantResolver>();
    services.AddMultiTenantDocumentStore(tenantId=>newDocumentStoreOptions
    {
    DatabaseProvider =newSqliteDatabaseProvider($"Data Source={tenantId}.db")
    });

    Both require an ITenantResolver implementation:

    publicclassMyTenantResolver(IHttpContextAccessorhttp) : ITenantResolver
    {
    publicstringGetCurrentTenant()
    => http.HttpContext?.User.FindFirst("tenant_id")?.Value
    ??thrownewInvalidOperationException("No tenant context");
    }

    Or instantiate directly (no DI needed):

  3. Inject IDocumentStore and start using it:

    publicclassMyService(IDocumentStorestore)
    {
    publicasyncTaskSaveUser(Useruser)
    {
    await store.Insert(user); // Id auto-generated for Guid/int/long; string Ids must be set
    }
    publicasyncTask<User?> GetUser(stringid)
    {
    returnawait store.Get<User>(id);
    }
    publicasyncTask<IReadOnlyList<User>> GetActiveUsers()
    {
    returnawait store.Query<User>()
    .Where(u=> u.IsActive)
    .OrderBy(u=> u.Name)
    .ToList();
    }
    }
PropertyTypeDefaultDescription
DatabaseProviderIDatabaseProvider (required)โ€”The database provider to use (e.g. SqliteDatabaseProvider, SqlCipherDatabaseProvider, SqlServerDatabaseProvider, MySqlDatabaseProvider, PostgreSqlDatabaseProvider, DuckDbDatabaseProvider). LiteDB, CosmosDB, MongoDB, and IndexedDB use their own options classes.
TableNamestring"documents"Default table name for all document types not mapped via MapTypeToTable
TypeNameResolutionTypeNameResolutionShortNameHow type names are stored (ShortName or FullName)
JsonSerializerOptionsJsonSerializerOptions?nullJSON serialization settings. When a JsonSerializerContext is attached as the TypeInfoResolver, all methods auto-resolve type info from the context
UseReflectionFallbackbooltrueWhen false, throws InvalidOperationException if a type canโ€™t be resolved from the configured TypeInfoResolver instead of falling back to reflection. Recommended for AOT deployments
LoggingAction<string>?nullCallback invoked with every SQL statement executed
TenantIdAccessorFunc<string>?nullWhen 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

By default all document types share a single table. Use MapTypeToTable to give a type its own dedicated table. Tables are lazily created on first use. Two types cannot map to the same custom table.

varstore=newDocumentStore(newDocumentStoreOptions
{
DatabaseProvider =newSqliteDatabaseProvider("Data Source=mydata.db"),
TableName ="docs"// change the default table name (optional)
}
.MapTypeToTable<Order>("orders") // explicit table name
.MapTypeToTable<AuditLog>() // auto-derived table name "AuditLog"
// User stays in the default "docs" table
);

By default every document type must have a property named Id. Override that with a custom property using either MapTypeToTable<T>(...) (combined with a dedicated table) or MapIdProperty<T>(...) (the type stays in the default shared table). The two are independent โ€” use either, both, or neither.

varstore=newDocumentStore(newDocumentStoreOptions
{
DatabaseProvider =newSqliteDatabaseProvider("Data Source=mydata.db")
}
// Dedicated table + custom Id
.MapTypeToTable<Sensor>("sensors", s=> s.DeviceKey) // Guid DeviceKey as Id
.MapTypeToTable<Tenant>("tenants", t=> t.TenantCode) // string TenantCode as Id
// Default shared table + custom Id
.MapIdProperty<BlogPost>(p=> p.Slug) // string Slug as Id
);
OverloadDescription
MapTypeToTable<T>()Auto-derive table name from type name
MapTypeToTable<T>(string tableName)Explicit table name
MapTypeToTable<T>(Expression<Func<T, object>> idProperty)Auto-derive table + custom Id
MapTypeToTable<T>(string tableName, Expression<Func<T, object>> idProperty)Explicit table + custom Id
MapIdProperty<T>(Expression<Func<T, object>> idProperty)Custom Id only โ€” type stays in the default shared table
MapIdProperty<T>(string propertyName)AOT-safe string overload

All overloads return DocumentStoreOptions for fluent chaining. Duplicate table names throw InvalidOperationException.

services.AddDocumentStore(opts=>
{
opts.DatabaseProvider =newSqliteDatabaseProvider("Data Source=mydata.db");
opts.MapTypeToTable<User>();
opts.MapTypeToTable<Order>("orders");
opts.MapTypeToTable<Sensor>("sensors", s=> s.DeviceKey);
});

Step 1 โ€” Add the marketplace:

claude plugin marketplace add shinyorg/skills

Step 2 โ€” Install the plugin:

claude plugin install shiny-data@shiny

Step 1 โ€” Add the marketplace:

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

Step 2 โ€” Install the plugin:

copilot plugin install shiny-data@shiny
View shiny-data Plugin