Skip to content

Document DB

A lightweight, database-agnostic document store for .NET that turns your database into a schema-free JSON document database with LINQ querying 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, four database providers.

  • GitHub stars for shinyorg/DocumentDb
  • NuGet package Shiny.DocumentDb
  • NuGet package Shiny.DocumentDb.Sqlite
  • NuGet package Shiny.DocumentDb.SqlServer
  • NuGet package Shiny.DocumentDb.MySql
  • NuGet package Shiny.DocumentDb.PostgreSql
  • NuGet package Shiny.DocumentDb.Extensions.DependencyInjection
  • Multi-provider — SQLite, SQL Server, MySQL, and PostgreSQL with a single API
  • 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
  • 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 mappingMapTypeToTable<T>() gives a document type its own dedicated table. Unmapped types share a configurable default table
  • Custom Id propertiesMapTypeToTable<T>("table", x => x.MyProp) uses an alternate property as the document Id
  • 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
  • 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
  • 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
  • TransactionsRunInTransaction with automatic commit/rollback
  • Batch insertBatchInsert inserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure
  • Hot backupBackup copies the database using the SQLite Online Backup API (SQLite only; other providers throw NotSupportedException)
  1. Install the NuGet packages

    Install the core package plus your provider:

    Terminal window
    dotnet add package Shiny.DocumentDb.Sqlite

    Each provider package includes the core Shiny.DocumentDb package and DI extensions automatically.

    A standalone DI package is also available for provider-agnostic registration:

    Terminal window
    dotnet add package Shiny.DocumentDb.Extensions.DependencyInjection
  2. Register with dependency injection:

    using Shiny.DocumentDb.Sqlite;
    services.AddSqliteDocumentStore("Data Source=mydata.db");
    // or with full options
    services.AddSqliteDocumentStore(opts =>
    {
    opts.DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db");
    opts.TypeNameResolution = TypeNameResolution.FullName;
    opts.JsonSerializerOptions = new JsonSerializerOptions
    {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
    });

    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();
    }
    }
PropertyTypeDefaultDescription
DatabaseProviderIDatabaseProvider (required)The database provider to use (e.g. SqliteDatabaseProvider, SqlServerDatabaseProvider, MySqlDatabaseProvider, PostgreSqlDatabaseProvider)
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

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

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

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

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

An AI skill is available for Shiny Document DB to help generate queries, configure stores, and follow best practices directly in your IDE.

Claude Code

Terminal window
claude plugin add github:shinyorg/skills

GitHub Copilot — Copy the shiny-sqlitedocumentdb skill file into your repository’s custom instructions.