Skip to content

SQLite Document DB

A lightweight SQLite-based document store for .NET that turns SQLite 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.

  • 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 with json_object 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
  • Surgical field updatesSetProperty updates a single JSON field via json_set() without deserialization. RemoveProperty strips a field via json_remove(). Both support nested paths
  • 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
  • 10-30x faster nested inserts vs sqlite-net — one write per document vs multiple table inserts
  • TransactionsRunInTransaction with automatic commit/rollback

Entity Framework Core is a natural choice for server-side .NET, but it becomes a liability on .NET MAUI platforms (iOS, Android, Mac Catalyst).

  • No AOT support. EF Core relies on runtime reflection and dynamic code generation for change tracking, query compilation, and model building. Its public API carries [RequiresDynamicCode] and [RequiresUnreferencedCode] attributes. On iOS, where Apple prohibits JIT compilation entirely, this is a non-starter for fully native AOT deployments.
  • Migrations add complexity without value. On a mobile device, the database is created on first launch or ships inside the app bundle. EF Core’s migration pipeline (Add-Migration, Update-Database, __EFMigrationsHistory) solves a problem that doesn’t exist here.
  • Heavy dependency graph. EF Core pulls in Microsoft.EntityFrameworkCore, its SQLite provider, design-time packages, and their transitive dependencies — increasing app bundle size on platforms where download size matters.
  • Relational overhead for document-shaped data. Mobile apps typically store user preferences, cached API responses, offline data queues, and local state. This data is naturally nested and variable. Normalizing it into tables with foreign keys and JOINs adds accidental complexity.
ConcernEF CoreShiny.SqliteDocumentDb
AOT / trimmingReflection-heavy; no AOT supportAll JsonTypeInfo<T> params optional; auto-resolves from context
MigrationsRequired for every schema changeNot needed — schema-free JSON
Nested objectsNormalized tables, foreign keys, JOINsSingle document, single write, single read
App bundle sizeLarge dependency treeSingle dependency on Microsoft.Data.Sqlite
Startup timeDbContext model building, migration checksOpen connection and go

Ahead-of-Time compilation is not optional on Apple platforms — iOS, iPadOS, tvOS, and Mac Catalyst all prohibit JIT at the OS level. Android benefits from AOT (PublishAot or AndroidEnableProfiledAot) with faster startup and lower memory usage.

The .NET trimmer removes unreferenced code to shrink the app binary. Libraries that depend on reflection break under trimming because the trimmer cannot statically determine which types are accessed at runtime. This forces developers to either disable trimming (larger binaries) or maintain complex trimmer XML files.

This library avoids both problems:

  • Source-generated JSON serialization. The JsonSerializerContext pattern generates serialization code at compile time. The trimmer and AOT compiler can see every type and code path.
  • No runtime expression compilation. LINQ expressions are translated to SQL strings by a visitor — no Expression.Compile(), no Reflection.Emit, no dynamic delegates.
  • No model building. There is no equivalent of EF Core’s OnModelCreating that discovers entities through reflection at startup.
  1. Install the NuGet package

    Terminal window
    dotnet add package Shiny.SqliteDocumentDb
  2. Register with dependency injection:

    services.AddSqliteDocumentStore("Data Source=mydata.db");
    // or with full options
    services.AddSqliteDocumentStore(opts =>
    {
    opts.ConnectionString = "Data Source=mydata.db";
    opts.TypeNameResolution = TypeNameResolution.FullName;
    opts.JsonSerializerOptions = new JsonSerializerOptions
    {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
    });
    // AOT-safe — attach a JsonSerializerContext so all methods auto-resolve type info
    var ctx = new AppJsonContext(new JsonSerializerOptions
    {
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    });
    services.AddSqliteDocumentStore(opts =>
    {
    opts.ConnectionString = "Data Source=mydata.db";
    opts.JsonSerializerOptions = ctx.Options;
    opts.UseReflectionFallback = false; // throw instead of using reflection for unregistered types
    });

    Or instantiate directly:

    var store = new SqliteDocumentStore(new DocumentStoreOptions
    {
    ConnectionString = "Data Source=mydata.db"
    });
  3. Inject IDocumentStore and start using it:

    public class MyService(IDocumentStore store)
    {
    public async Task SaveUser(User user)
    {
    await store.Set(user); // user.Id is auto-generated if default
    }
    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
ConnectionStringstring (required)SQLite connection string
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

An AI skill is available for Shiny SQLite 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.