Why DocumentDb
If you are storing data in a .NET app today, you are most likely reaching for sqlite-net-pcl (the de-facto mobile SQLite ORM) or Entity Framework Core (the default relational stack on the server). Shiny.DocumentDb is a different shape of tool: a schema-free document store that persists entire object graphs — nested objects and child collections — as a single JSON document, queryable with LINQ, across twelve database backends, under full AOT and trimming.
This page lays out, honestly, where each tool fits and where DocumentDb pulls ahead.
Feature matrix
Section titled “Feature matrix”| Capability | Shiny.DocumentDb | sqlite-net-pcl | EF Core |
|---|---|---|---|
| Schema management | Zero — store objects directly | Auto-creates flat tables from POCOs | Migrations (Add-Migration / Update-Database) |
| Nested objects & child collections | One JSON document, one read/write | Not supported — separate tables + manual joins | Owned types / related entities + JOINs |
| LINQ on nested data | Where(o => o.Lines.Any(l => l.Price > 10)) | Not possible | Yes, via Include() + LINQ→SQL |
| Database providers | SQLite, LiteDB, CosmosDB, MongoDB, Azure Table, DynamoDB, DuckDB, IndexedDB, MySQL, SQL Server, PostgreSQL, Oracle | SQLite only | Many relational + CosmosDB |
| Typed context (EF-style) | DocumentContext + DocumentSet<T>, source-generated from [Document] | No — flat SQLiteConnection API | DbContext + DbSet<T> |
| AOT / trimming | First-class — source-generated JSON, zero reflection | Reflection-based; no AOT support | [RequiresDynamicCode] / [RequiresUnreferencedCode]; no full AOT |
| Migrations | None — schema-free JSON | Manual (you own every ALTER) | First-class, but required for every schema change |
| Change tracking | None — snapshot writes | None | Full graph tracking (powerful, but has a cost) |
| Change feeds / monitoring | NotifyOnChange<T> everywhere; native feeds on PostgreSQL, SQL Server, Cosmos, DynamoDB Streams | No | No (interceptors only) |
| Global query filters | AddQueryFilter<T> (soft-delete, tenant scoping) | No | HasQueryFilter |
| Compiled queries | N/A — expressions compile to SQL strings via a visitor, no per-query JIT | N/A | Yes — EF.CompileAsyncQuery |
| Projections | SQL-level json_object via .Select() | Manual | .Select() |
| Transactions | store.CreateUnitOfWork() + SaveChanges() | RunInTransactionAsync | SaveChanges / explicit transactions |
| Vector / ANN search | Yes — cross-provider (pgvector, sqlite-vec, DiskANN, …) | No | Provider-specific only |
| Full-text search | Yes — MapFullTextProperty + relevance-ranked search on every provider (FTS5, tsvector, Oracle Text, …) | No | Provider-specific only |
| Temporal history | Yes — MapTemporal (history / as-of / diff / restore), all providers | No | SQL Server temporal tables only |
| Multi-tenancy | Built-in — shared-table or tenant-per-database via ITenantResolver | No | Manual (query filters / connection routing) |
| Offline-first sync | Shiny.DocumentDb.AppDataSync — two-way HTTP sync on client providers | No | No |
| Query endpoints | OData v4 + AI-tool functions out of the box | No | OData via separate stack |
| Telemetry | Built-in OpenTelemetry metrics + spans | No | EF diagnostics / interceptors |
| Dependency footprint | Core + one provider package | Single small package | Large transitive graph |
| Startup cost | Open a connection and go | Open a connection and go | DbContext model building + migration checks |
| Best fit | Object graphs, nested data, mobile/offline, multi-provider | Simple flat-table CRUD on mobile | Server-side relational apps |
Where DocumentDb wins
Section titled “Where DocumentDb wins”Nested data is free. An order with a shipping address, line items, and tags is one document. One Insert, one Get, one round trip. sqlite-net forces you into three normalized tables with foreign keys and manual rehydration on every read; EF Core models the relationships for you but still pays for multi-table JOINs and change-tracking graph fixup. The benchmarks below show this gap widening as the graph grows.
No migrations, no model building. On a mobile device the database is created on first launch — there is no DBA, no staging environment, no rollback plan. EF Core’s migration pipeline adds ceremony with no payoff in that world, and its OnModelCreating reflection runs at startup. DocumentDb opens a connection and is ready.
AOT and trimming actually work. Apple platforms prohibit JIT outright; Android benefits heavily from AOT. EF Core is reflection- and dynamic-code-heavy and carries trim/AOT-hostile attributes throughout its API; sqlite-net relies on reflection too. DocumentDb uses source-generated JsonTypeInfo<T> on every API and translates LINQ to SQL with a visitor — no Expression.Compile(), no Reflection.Emit.
One API, twelve backends. The same code runs on SQLite on a phone, PostgreSQL on a server, and Cosmos, DynamoDB, or Azure Table in the cloud. sqlite-net is SQLite-only; EF Core spans relational engines but with provider-specific quirks and a heavier footprint.
The batteries are included. Full-text search, vector/ANN search, temporal history, multi-tenancy, global query filters, change feeds, offline-first HTTP sync, OData endpoints, LLM tool functions, JSON Schema validation, and OpenTelemetry are all first-party and work across the provider set — not features you bolt on with a second library and reconcile against your storage layer. With sqlite-net or EF Core, each of those is a separate integration you own.
Benchmarks
Section titled “Benchmarks”Measured with BenchmarkDotNet v0.15.8 on Apple M5 Pro, .NET 10.0.8, macOS. All four contenders run against SQLite. EF Core uses its fastest read path: pre-compiled EF.CompileAsyncQuery queries with AsNoTracking; Dapper runs hand-written SQL over a raw Microsoft.Data.Sqlite connection. Source lives in the benchmarks/ folder of the repo.
Flat POCO (single table)
Section titled “Flat POCO (single table)”A plain User { Id, Name, Age, Email } — the case where sqlite-net and EF Core are at their strongest, because they read native indexed columns while the document store extracts from JSON.
Insert (loop of single inserts)
Section titled “Insert (loop of single inserts)”| Library | 10 | 100 | 1000 |
|---|---|---|---|
| Shiny.DocumentDb | 389 µs | 3.54 ms | 35.14 ms |
| EF Core | 877 µs | 7.28 ms | 93.02 ms |
| Dapper | 1.70 ms | 17.16 ms | 385.10 ms |
| sqlite-net | 1.78 ms | 17.65 ms | 260.62 ms |
| Operation | Shiny.DocumentDb | Dapper | EF Core (compiled) | sqlite-net |
|---|---|---|---|---|
| Get by Id | 2.73 µs | 4.96 µs | 8.69 µs | 10.35 µs |
| Get all (1000) | 500.26 µs | 315.88 µs | 317.24 µs | 396.97 µs |
| Query by name (1000) | 160.52 µs | 21.47 µs | 25.13 µs | 31.19 µs |
Nested object graph (Order → Address + Order Lines + Tags)
Section titled “Nested object graph (Order → Address + Order Lines + Tags)”One order with a shipping address, three line items, and two tags. sqlite-net stores this across 3 tables (6 inserts per order, 3 queries + manual rehydration per read). EF Core models it as related entities read with Include. Dapper issues hand-written SQL across the same 3 tables. DocumentDb stores it as one JSON document.
Insert (nested)
Section titled “Insert (nested)”| Library | 10 | 100 | 1000 |
|---|---|---|---|
| Shiny.DocumentDb | 417 µs | 3.63 ms | 36.31 ms |
| EF Core (3 tables) | 4.94 ms | 24.75 ms | 648.55 ms |
| Dapper (3 tables) | 11.43 ms | 174.28 ms | 1.99 s |
| sqlite-net (3 tables) | 11.73 ms | 123.38 ms | 2.18 s |
At 1,000 orders the document store is ~18x faster than EF Core, ~55x faster than Dapper, and ~60x faster than sqlite-net.
Read (nested)
Section titled “Read (nested)”| Operation | Shiny.DocumentDb | sqlite-net | Dapper | EF Core (Include, compiled) |
|---|---|---|---|---|
| Get by Id | 3.47 µs | 27.84 µs | 18.56 µs | 32.03 µs |
| Get all (100) | 127.2 µs | 205.7 µs | 253.6 µs | 1.13 ms |
| Get all (1000) | 1.36 ms | 1.76 ms | 2.45 ms | 11.88 ms |
| Query by status (~500/1000) | 1.01 ms | 1.42 ms | 2.13 ms | 5.90 ms |
The document store wins every nested read — by 6–9x against EF Core’s Include joins, and 1.8–5x even against Dapper’s hand-written SQL — because the whole graph lives in one row and deserializes in a single pass, with no JOINs, no manual rehydration, and no change-tracker graph fixup.
When to choose each
Section titled “When to choose each”- Choose Shiny.DocumentDb when your data is document-shaped (nested objects, child collections, variable structure), when you target .NET MAUI / AOT, when you want one storage API across a dozen databases, or when you want vector and full-text search, temporal history, multi-tenancy, change feeds, offline sync, or built-in telemetry without bolting on more libraries.
- Choose sqlite-net-pcl for the simplest possible flat-table CRUD on a single SQLite database, where every entity is a handful of scalar columns and you never store an object graph.
- Choose EF Core for server-side relational applications with complex cross-entity queries, reporting, and a relational schema you genuinely want to model and migrate.
- Choose Dapper when you want full control over hand-written SQL on a relational schema and are happy to map and rehydrate object graphs yourself — it is the fastest relational floor for flat reads, but you own every JOIN and every
INSERT.
DocumentDb is not trying to replace a relational mapper for relational problems. It is the right tool when your objects are the model and the database is just where they live.