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, 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.
| GitHub | |
| Downloads | |
| SQLite | |
| SQLCipher | |
| SQL Server | |
| MySQL | |
| PostgreSQL | |
| LiteDB | |
| CosmosDB | |
| DI Extensions | |
| IndexedDB | |
| AI Tools |
Features
Section titled “Features”- Multi-provider — SQLite, SQLCipher (encrypted SQLite), LiteDB, CosmosDB, IndexedDB (Blazor WASM), SQL Server, MySQL, and PostgreSQL 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 viaSql.*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 SQLLIMIT/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)uses an alternate property as the document Id - 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 - Typed Id lookups —
Get,Remove,SetProperty, andRemovePropertyaccept the Id asobjectso you can pass aGuid,int,long, orstringdirectly. Unsupported types throwArgumentException - Full AOT/trimming support — all
JsonTypeInfo<T>parameters are optional and auto-resolve from a configuredJsonSerializerContext. SetUseReflectionFallback = falseto 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. ThrowsConcurrencyExceptionon conflict. Works across all providers — stored in the JSON blob with zero schema changes - Transactions —
RunInTransactionwith automatic commit/rollback - Batch insert —
BatchInsertinserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure - Spatial / geo queries —
WithinRadius,WithinBoundingBox, andNearestNeighborswithGeoPointsupport. SQLite uses R*Tree; CosmosDB uses nativeST_DISTANCE/ST_WITHIN. Learn more - Hot backup —
Backupcopies the database to a file. Available onSqliteDocumentStore,SqlCipherDocumentStore, andLiteDbDocumentStore - Clear all —
SqliteDocumentStore.ClearAllAsync()deletes all documents across all tables including spatial sidecar data - SQLCipher encryption — separate
Shiny.DocumentDb.Sqlite.SqlCipherpackage with AES-256 encryption, password-aware backup, andRekeyAsyncto change the encryption key - Multi-tenancy — two isolation strategies: shared-table (single database with automatic
TenantIdcolumn filtering) and tenant-per-database (separate database per tenant via lazy factory). Both resolve the current tenant via a user-implementedITenantResolver. Consumer code is unchanged — tenant isolation is applied transparently - 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. 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.PostgreSqlTerminal window dotnet add package Shiny.DocumentDb.IndexedDbEach provider package includes the core
Shiny.DocumentDbpackage automatically.For dependency injection, also install the DI extensions package:
Terminal window dotnet add package Shiny.DocumentDb.Extensions.DependencyInjection -
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 PostgreSqlDatabaseProvider("Host=localhost;Database=mydb;Username=postgres;Password=pass;");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 withIDocumentStoreProvider:public class MyService([FromKeyedServices("users")] IDocumentStore userStore,[FromKeyedServices("analytics")] IDocumentStore analyticsStore) { }// Or dynamically:public class MyService(IDocumentStoreProvider 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);// Tenant-per-database: separate database per tenant (scoped IDocumentStore)services.AddSingleton<ITenantResolver, MyTenantResolver>();services.AddMultiTenantDocumentStore(tenantId => new DocumentStoreOptions{DatabaseProvider = new SqliteDatabaseProvider($"Data Source={tenantId}.db")});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;")}); -
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, PostgreSqlDatabaseProvider) |
TableName | string | "documents" | Default table name for all document types not mapped via MapTypeToTable |
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 |
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 |
Table-Per-Type Mapping
Section titled “Table-Per-Type Mapping”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.
var store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("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);Custom Id property
Section titled “Custom Id property”By default every document type must have a property named Id. When mapping a type to a table, you can also specify a custom Id property via an expression. Custom Id requires a table mapping.
var store = new DocumentStore(new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")}.MapTypeToTable<Sensor>("sensors", s => s.DeviceKey) // Guid DeviceKey as Id.MapTypeToTable<Tenant>("tenants", t => t.TenantCode) // string TenantCode as Id);MapTypeToTable overloads
Section titled “MapTypeToTable overloads”| Overload | Description |
|---|---|
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 |
All overloads return DocumentStoreOptions for fluent chaining. Duplicate table names throw InvalidOperationException.
DI Registration with Table Mapping
Section titled “DI Registration with Table Mapping”services.AddDocumentStore(opts =>{ opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db"); opts.MapTypeToTable<User>(); opts.MapTypeToTable<Order>("orders"); opts.MapTypeToTable<Sensor>("sensors", s => s.DeviceKey);});AI Coding Assistant
Section titled “AI Coding Assistant”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