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
  • LINQ expression querieso => o.ShippingAddress.City == "Portland" translates to json_extract SQL automatically
  • IAsyncEnumerable<T> streaming — yield results one-at-a-time with GetAllStream and QueryStream
  • Expression-based JSON indexes — up to 30x faster queries on indexed properties
  • SQL-level projections — project into DTOs with json_object at the database level
  • 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 — every API has a JsonTypeInfo<T> overload for source-generated JSON. Configure a JsonSerializerContext once and all overloads auto-resolve type info — no per-call JsonTypeInfo<T> needed. 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 supportEvery API has a JsonTypeInfo<T> overload; zero reflection
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 overloads 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-1", user, ctx.User);
    }
    public async Task<User?> GetUser(string id)
    {
    return await store.Get<User>(id, ctx.User);
    }
    }
PropertyTypeDefaultDescription
ConnectionStringstring (required)SQLite connection string
TypeNameResolutionTypeNameResolutionShortNameHow type names are stored (ShortName or FullName)
JsonSerializerOptionsJsonSerializerOptions?camelCase, no indentJSON serialization settings. When a JsonSerializerContext is attached as the TypeInfoResolver, overloads without JsonTypeInfo<T> 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