Limitations
This page collects what the library does not do, so you can rule it out (or in) before adopting it. Where a limitation is provider-specific, see Provider Reference for the full matrix.
Design model
Section titled “Design model”DocumentDB is a document store, not a relational ORM. A few things follow from that and are not going to change:
- No JOINs across document types. Store related data on the same document (embed child collections) or denormalize. Cross-document lookups happen with two
Getcalls. - No referential integrity / foreign keys. Nothing in the schema enforces that a
CustomerIdon anOrderpoints to an existingCustomer. Validate in application code. - No schema, no migrations. The flip side: removing a property from your C# type does not delete it from stored JSON. Old fields just stop being deserialized. Use
RemovePropertyif you need to physically strip them. - One document type per table is optional, not enforced. By default all types share the
documentstable and are discriminated byTypeName. Mixing types on the same table prevents the database engine from using a per-type clustered key.
Document & API constraints
Section titled “Document & API constraints”- Every document type must expose a public
Idproperty of typeGuid,int,long, orstring. Alternative names are supported viaMapTypeToTable<T>("tbl", x => x.MyKey)but only when a custom table mapping is also set. stringIds must be supplied beforeInsert— there is no auto-generation strategy for them.- The library is async-only. There are no synchronous overloads.
- One default table per store. All unmapped types share
DocumentStoreOptions.TableName. - Two types cannot map to the same custom table. Throws
InvalidOperationExceptionat registration. SetPropertyis scalar-only. Supported value types:string,int,long,double,float,decimal,bool,null. To replace a nested object or collection, useUpdate(full replacement) orUpsert(merge — see provider note below).UnitOfWorkis not thread-safe. Use one instance per logical operation / request scope.- Streaming methods on shared-connection providers hold the internal semaphore for the duration of enumeration. Do not interleave other store operations inside the same
await foreachagainst SQLite, SQLCipher, or DuckDB stores. Server SQL providers (Postgres / MySQL / SQL Server) open the streaming connection from the driver pool and do not block other callers.
Indexing
Section titled “Indexing”CreateIndexAsynconly exists on the concrete SQLDocumentStoretypes (SQLite, SQLCipher, PostgreSQL, SQL Server, MySQL, DuckDB). It is not onIDocumentStore.- No
INCLUDEcolumns, no filtered indexes beyond the automaticWHERE TypeName = '...'filter applied by the library. - CosmosDB, MongoDB, LiteDB, IndexedDB do not expose
CreateIndexAsync. CosmosDB indexes everything automatically (tune via container indexing policy). MongoDB indexes are managed with native Mongo tooling. LiteDB does not configure indexes. IndexedDB indexes are fixed at object-store creation time (when you bumpVersion).
Queries
Section titled “Queries”The expression API translates a subset of C#. Anything outside this list throws NotSupportedException at query time:
Supported in .Where() / .Select() | Not supported |
|---|---|
==, !=, >, >=, <, <=, &&, ||, ! | string.ToLower(), ToUpper() |
null / != null | string.IsNullOrEmpty(), IsNullOrWhiteSpace() |
string.Contains, StartsWith, EndsWith (translated to LIKE) | string.Equals(other, StringComparison.*) |
Nested property access (o.Address.City) | Math.*, DateTime.AddDays, TimeSpan arithmetic |
collection.Any(), Any(predicate), Count(), Count(predicate) | Explicit casts beyond boxing ((int)x.Age) |
| Captured closure variables | string.Format, interpolation |
Enum equality (boxed via implicit Convert) | Select projecting into anonymous types or tuples |
DateTime equality / comparison (ISO-8601 ordering) | Custom method calls on your own types |
Sum/Min/Max/Average on child collections in .Select | GroupBy on multiple keys |
Single-key GroupBy | Join, SelectMany, Distinct, Union, Except, Zip |
If your predicate needs something on the right column of this table, fall back to raw SQL via Query<T>("...", parameters) — but note that raw SQL is not supported on LiteDB or IndexedDB.
Provider-specific limitations
Section titled “Provider-specific limitations”Full matrix on Provider Reference. The most consequential:
| Provider | What’s missing or different |
|---|---|
| PostgreSQL | Upsert is shallow merge only, not RFC 7396 deep merge. No spatial. No Backup(). |
| SQL Server | Same shallow Upsert. Requires SQL Server 2025+ / Azure SQL with native JSON type. No spatial. No Backup(). |
| MySQL | No spatial. No Backup(). |
| CosmosDB | No Backup(). No CreateIndexAsync (tune indexing policy on the container). Cost model is RU-per-operation — full scans over unindexed paths get expensive fast. |
| MongoDB | No raw SQL. No spatial. No CreateIndexAsync (manage indexes with native Mongo tooling). Transactions are compensating on single-node deployments — only inserts are tracked and rolled back; updates and removes are not. Use a replica set + custom IClientSessionHandle for true ACID. |
| DuckDB | Single-process / writer-one (like SQLite). No spatial. No Backup() (copy the file directly or use DuckDB EXPORT DATABASE). SetProperty/RemoveProperty go through json_merge_patch rather than json_set/json_remove. |
| LiteDB | All predicates evaluated in C# after a full load of every document of that type. No raw SQL. No spatial. Fine for small datasets, painful for large ones. |
| IndexedDB | Same client-side predicate evaluation as LiteDB. No raw SQL. No spatial. No CreateIndexAsync. Browser quota typically ~50 MB before prompting. Single-tab writer. |
Concurrency & transactions
Section titled “Concurrency & transactions”- SQLite / DuckDB stores keep a single long-lived connection and serialize every operation through a
SemaphoreSlim— embedded engines lock the whole database on writes, so multi-flighting buys nothing. Two simultaneous callers queue. - Postgres / MySQL / SQL Server stores open a fresh
DbConnectionper operation and let the ADO.NET driver pool multiplex callers. A single store instance is safe to share across threads and operations execute concurrently up to the pool size. - LiteDB is single-process: opening the same file from two processes fails.
- IndexedDB is effectively single-tab — multi-tab writes can interleave non-deterministically.
- CosmosDB / MongoDB use their respective documented thread-safe clients (
CosmosClient,IMongoClient) and pool internally. - Nested transactions are not supported. Calling
RunInTransactioninside a transaction reuses the outer transaction (noSAVEPOINT). - Optimistic concurrency (
MapVersionProperty) checks the version onUpdateandUpsert-as-update only. It does not protectSetProperty,RemoveProperty,ExecuteUpdate, orExecuteDelete— those are unchecked bulk operations.
Backup
Section titled “Backup”Backup() is only on the concrete store types: SqliteDocumentStore, SqlCipherDocumentStore, LiteDbDocumentStore. Not on IDocumentStore. Not on the server-database providers (use their native tooling: pg_dump, BACKUP DATABASE, mysqldump, etc.).
What about size?
Section titled “What about size?”There is no built-in cap on document size, but each provider has practical limits:
| Provider | Practical max single-document size |
|---|---|
| SQLite | ~1 GB (SQLITE_MAX_LENGTH, configurable but rarely worth it) |
PostgreSQL JSONB | 255 MB (toasted) |
SQL Server JSON | 2 GB (max size) |
MySQL JSON | 1 GB |
DuckDB JSON | Practically bounded by available memory during reads (vectorized engine) |
| CosmosDB | 2 MB per document — the hard one to plan around |
| MongoDB BSON | 16 MB per document — hard server limit |
| LiteDB | 1 MB per document, 256 MB per page chain |
| IndexedDB | Browser-dependent, typically tens of MB total quota |
If you have documents approaching these, split them. The library does not page large blobs for you.