Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Check It Out

Provider Reference

The expression API is provider-agnostic — the same C# query compiles against every provider. What differs is the SQL each provider emits, the storage type of the JSON column, and a handful of behavioral edge cases. This page lays them out side-by-side so you can pick the right provider, predict generated SQL, and avoid provider-specific surprises.

For provider-specific install, setup, and feature notes see the per-provider pages:

ProviderDoc
SQLiteSQLite
SQLCipher (encrypted SQLite)SQLCipher
PostgreSQLPostgreSQL
SQL ServerSQL Server
MySQLMySQL
OracleOracle
DuckDBDuckDB
Azure Cosmos DBCosmos DB
MongoDBMongoDB
LiteDBLiteDB
IndexedDB (Blazor WASM)IndexedDB
CapabilitySQLiteSQLCipherPostgreSQLSQL ServerMySQLOracleDuckDBCosmosDBMongoDBLiteDBIndexedDB
Storage column typeTEXTTEXTJSONBJSON (SQL Server 2025+ / Azure SQL)JSONCLOB + IS JSON check (Oracle 23ai+)JSON (DuckDB native)native JSON (string data envelope)BsonDocumentBsonDocumentobject store record
Query<T>(string sql, ...) raw SQLyesyesyesyesyesyesyesyes (Cosmos SQL)nonono
CreateIndexAsync<T>(x => x.Prop)yesyesyesyes (persisted computed column + filtered index)yesyes (function-based index on JSON_VALUE)yes (functional index on json_extract_string)n/a (Cosmos auto-indexes)n/a (Mongo indexes manually)n/an/a (schema-time only)
Composite / multi-column indexyesyesyesyes (persisted computed columns + composite index)yesyesyesn/a (Cosmos auto-indexes)n/a (Mongo indexes manually)n/an/a (schema-time only)
Predicate translated to SQLyesyesyesyesyesyesyesyespartial — server-side filter + C# evaluationno — runs in C# after loadno — runs in C# after load
Upsert deep merge (RFC 7396)yes (json_patch)yesshallow only (jsonb ||)shallow only (hand-rolled OPENJSON)yes (JSON_MERGE_PATCH)yes (JSON_MERGEPATCH)yes (json_merge_patch)yes (C# recursive merge)yes (C# recursive merge)yes (C# recursive merge)yes (C# recursive merge)
Spatial (WithinRadius, etc.)yes (R*Tree sidecar)yesnononononoyes (native GeoJSON)nonono
Hot Backup()yesyes (encrypted)nononononononoyesno
Concurrent writersreader-many / writer-onesameyesyesyesyesreader-many / writer-oneyesyessingle-processsingle browser tab
Transactionsnativenativenativenativenativenativenativecompensating (track inserts, undo on failure)compensating (replica-set required for native ACID)bulk insert in one txone IDB transaction
BatchInsert chunk size500500500500500500 (multi-row VALUES, 23ai+)500100 (Cosmos TransactionalBatch limit)InsertMany (ordered)bulk insert in one txone IDB transaction

The JSON column is always named Data across every SQL provider. Companion columns are Id, TypeName, CreatedAt, UpdatedAt.

The operations below all run inside the generated SQL — you never write these yourself with the LINQ builder. They matter when you write raw SQL via Query<T>("...", parameters) or when debugging via the Logging callback.

ProviderGenerated expression
SQLite / SQLCipherjson_extract(Data, '$.prop')
PostgreSQLData #>> '{prop}'
SQL ServerJSON_VALUE(Data, '$.prop')
MySQLNULLIF(JSON_UNQUOTE(JSON_EXTRACT(Data, '$.prop')), 'null')
OracleJSON_VALUE(Data, '$.prop')
DuckDBjson_extract_string(Data, '$.prop')
CosmosDBc.data.prop
MongoDBdata.prop (translated to BSON field path)
ProviderFunction
SQLite / SQLCipherjson_set(Data, '$.prop', @value)
PostgreSQLjsonb_set(Data, '{prop}', to_jsonb(@value))
SQL ServerJSON_MODIFY(Data, '$.prop', @value)
MySQLJSON_SET(Data, '$.prop', CAST(@value AS JSON))
Oracleshiny_json_set(Data, @path, @value) (PL/SQL helper — JSON_TRANSFORM cannot take a bound path)
DuckDBjson_merge_patch(Data, '{"prop":@value}'::JSON) (path folded into a synthetic merge patch)
MongoDB$set: { "data.prop": @value }
ProviderFunction
SQLite / SQLCipherjson_remove(Data, '$.prop')
PostgreSQLData #- string_to_array('prop', ',')
SQL ServerJSON_MODIFY(Data, '$.prop', NULL)
MySQLJSON_REMOVE(Data, '$.prop')
Oracleshiny_json_remove(Data, @path) (PL/SQL helper wrapping JSON_TRANSFORM ... REMOVE)
DuckDBjson_merge_patch(Data, '{"prop":null}'::JSON) (RFC 7396 null = delete)
MongoDB$unset: { "data.prop": 1 }

Iterate child collections (Any / Count / collection aggregates)

Section titled “Iterate child collections (Any / Count / collection aggregates)”
ProviderMechanism
SQLite / SQLCipherjson_each(Data, '$.lines')
PostgreSQLjsonb_array_elements(Data #> '{lines}')
SQL ServerOPENJSON(Data, '$.lines')
MySQLJSON_TABLE(Data, '$.lines[*]' COLUMNS(value JSON PATH '$'))
OracleJSON_TABLE(Data, '$.lines[*]' COLUMNS (jval CLOB FORMAT JSON PATH '$', sval VARCHAR2(4000) PATH '$'))
DuckDBunnest(CAST(json_extract(Data, '$.lines') AS JSON[]))

Upsert is documented as RFC 7396 JSON Merge Patch (deep merge of objects, scalar/array replacement). That holds for SQLite, MySQL, Oracle, DuckDB, CosmosDB, MongoDB, LiteDB, and IndexedDB. It does not hold for PostgreSQL or SQL Server, which only perform a shallow top-level merge.

What this means in practice — given an existing document:

{ "name": "Alice", "address": { "city": "Portland", "zip": "97201" } }

and an Upsert patch:

{ "address": { "city": "Seattle" } }
ProviderResult of address after upsert
SQLite / MySQL / Oracle / DuckDB / CosmosDB / MongoDB / LiteDB / IndexedDB{ "city": "Seattle", "zip": "97201" } (deep merge — zip preserved)
PostgreSQL / SQL Server{ "city": "Seattle" } (shallow merge — zip lost)

SQL Server cannot index a JSON_VALUE expression directly, so CreateIndexAsync<T>(x => x.SomeProperty) adds a PERSISTED computed column and builds a filtered index on it:

ALTER TABLE [documents]
ADD cc_idx_json_User_name AS CAST(JSON_VALUE(Data, '$.name') AS NVARCHAR(450));
CREATE INDEX idx_json_User_name
ON [documents] (cc_idx_json_User_name)
WHERE TypeName = N'User';

DropIndexAsync drops both the index and the backing computed column. The other SQL providers (SQLite, PostgreSQL, MySQL) emit partial expression / functional indexes — see Indexes for the exact DDL.

The provider creates the Data column as JSONB. This is the binary form: parsed once on write, stored without whitespace, key order not preserved. Lookups are fast and the column can be indexed with expression indexes (used by CreateIndexAsync). If you ever need GIN indexes (e.g. for @> containment queries from outside the library), add them yourself — the library does not.

The provider uses the new JSON storage type, not nvarchar(max). This requires SQL Server 2025 or later, or Azure SQL with the JSON-type preview enabled. Earlier SQL Server versions will fail at table creation.

MySQL’s JSON_EXTRACT returns a JSON-typed value, not a SQL string — comparing it directly to a bound parameter would fail. The provider always wraps extraction in NULLIF(JSON_UNQUOTE(...), 'null') so a stored JSON null and a missing key both surface as SQL NULL. Generally invisible — relevant only when you write raw SQL.

Oracle — CLOB with IS JSON check (23ai+)

Section titled “Oracle — CLOB with IS JSON check (23ai+)”

The provider stores Data as a CLOB constrained by IS JSON, the conventional Oracle pattern for large JSON documents. Oracle Database 23ai or later is required — the provider uses multi-row INSERT ... VALUES, CREATE INDEX IF NOT EXISTS, the JSON() constructor, and SQL BOOLEAN literals.

Two Oracle-specific mechanics are worth knowing:

  1. Dialect adapter — Oracle rejects @name placeholders and ODP.NET binds parameters by position by default. The provider wraps each connection in a delegating command adapter that rewrites @name:name, binds by name, strips trailing semicolons, and adds FROM DUAL where required. Raw SQL you write still uses @name like every other provider.
  2. SetProperty / RemoveProperty helpers — Oracle’s JSON_TRANSFORM only accepts literal path expressions, so the provider creates shiny_json_set / shiny_json_remove PL/SQL functions (idempotent CREATE OR REPLACE) that inline the path into a dynamic statement. The connecting user needs CREATE PROCEDURE privilege.

See Oracle for full details.

DuckDB stores the Data column as its built-in JSON type (text-backed, parsed on read). The json extension is loaded automatically on every connection (INSTALL json; LOAD json;). DuckDB 0.10+ exposes json_merge_patchUpsert uses it directly for true RFC 7396 deep merge with no read-merge-write round trip.

DuckDB has no json_set / json_remove. SetProperty and RemoveProperty fold the JSON path into a synthetic merge-patch document on the server using list_reduce(list_reverse(string_split(...)), ...) — same RFC 7396 semantics, different SQL. Generally invisible — relevant only when you’re reading the Logging callback.

DuckDB is single-process / reader-many / writer-one (similar to SQLite). It is not a multi-user server — pick it for embedded analytical workloads, on-device reporting, or pipelines that ingest Parquet/CSV via DuckDB’s native tooling alongside your document store.

CosmosDB — data is a stringified envelope

Section titled “CosmosDB — data is a stringified envelope”

The Cosmos document layout is:

{
"id": "...",
"typeName": "User",
"data": "{\"name\":\"Alice\",...}",
"createdAt": "...",
"updatedAt": "..."
}

Inside the SDK the data field is the serialized JSON of your document — accessed in Cosmos SQL as c.data.name. Partition key is /typeName. Cosmos auto-indexes every path by default; tune via the indexing policy on the container if cost matters.

The Mongo document layout mirrors the SQL envelope:

{
"_id": "{TypeName}:{Id}",
"id": "...",
"typeName": "User",
"data": { "name": "Alice", "age": 25, "...": "..." },
"createdAt": "...",
"updatedAt": "..."
}

_id is the composite Cosmos-style key — this guarantees uniqueness per type/Id pair across the shared "documents" collection (or any custom collection mapped via MapTypeToCollection<T>()). The data sub-document is a real BSON object (not a stringified envelope), so MongoDB queries, indexes, and aggregations can target nested fields natively.

LINQ predicates are translated to a MongoDB FilterDefinition for the type/sort/skip/take phase. Complex sub-expressions outside the translated subset are evaluated in C# after a typed find. Raw SQL is not supported — pass LINQ.

A UnitOfWork uses a compensating model rather than a multi-document session: inserts are tracked and deleted on failure, matching the CosmosDB provider. To use true ACID transactions, run MongoDB in a replica set and lift the constraint in your fork. Updates and removes in a unit are not compensated.

LiteDB / IndexedDB — predicates run client-side

Section titled “LiteDB / IndexedDB — predicates run client-side”

These two providers do not translate LINQ predicates to a query language. They:

  1. Load all documents of the type (filtered only by TypeName / object-store name) into memory.
  2. Apply the compiled predicate in C#.
  3. Apply ordering, pagination, and projection in C#.

For small datasets this is fine. For collections > a few thousand documents, expect linear scans on every query. There is no way to push a predicate down. If you need server-side filtering on a large dataset in Blazor WASM, use SQLite-in-WASM (see IndexedDB notes).

Query<T>(string whereClause, object? parameters) and QueryStream<T>(...) are supported by every SQL provider (SQLite, SQLCipher, PostgreSQL, SQL Server, MySQL, Oracle, DuckDB) plus CosmosDB. LiteDB, MongoDB, and IndexedDB throw NotSupportedException — use the LINQ-based Query<T>() overload instead.

Parameter syntax in the whereClause you write:

ProviderParameter prefix
SQLite, SQLCipher@name
PostgreSQL@name (Npgsql rewrites to $1 style internally)
SQL Server@name
MySQL@name
Oracle@name (the dialect adapter rewrites to :name before execution)
DuckDB@name
CosmosDB@name (Cosmos SQL native)
// Same C# call — provider rewrites the JSON extraction syntax under the hood
var results = await store.Query<User>(
/* SQLite */ "json_extract(Data, '$.name') = @name",
/* Postgres */ // "Data #>> '{name}' = @name"
/* SQL Server */ // "JSON_VALUE(Data, '$.name') = @name"
new { name = "Alice" });

When porting raw SQL across providers, the WHERE-clause syntax is yours to translate — only the LINQ builder is provider-agnostic.

SQLite quotes table identifiers with " so a type named Order or User works as a mapped table. The other providers follow their native quoting rules; if you hit a reserved-word collision, prefer an explicit non-reserved name via MapTypeToTable<T>("orders").

ConcernWhat differs
LIKE case sensitivitySQLite: ASCII case-insensitive by default. PostgreSQL / Oracle: case-sensitive (use ILIKE on PG if you need otherwise). SQL Server / MySQL: depends on column / database collation.
Stored JSON null vs missing keyDistinguishable in SQLite, PostgreSQL, SQL Server, Oracle. MySQL collapses both to NULL via the NULLIF(...,'null') wrapper.
jsonb key orderingPostgreSQL’s JSONB does not preserve key insertion order. Affects byte-for-byte diffs of the stored blob (does not affect GetDiff, which works at the C# object level).
Numeric coercionSQLite is dynamically typed — a JSON "42" string compares equal to integer 42. Other providers do not coerce.
Decimal precisionAll providers serialize/deserialize via System.Text.Json, so decimal precision is preserved as JSON numbers — but Postgres JSONB re-parses, and very large numerics may round-trip through double in extracted scalars. Prefer SetProperty for exact-value updates.
Use caseRecommendation
Mobile / desktop / single-processSQLite (or SQLCipher if data must be encrypted at rest)
ASP.NET app, owned databasePostgreSQL — JSONB indexability, mature concurrency
Existing SQL Server estate, on SQL Server 2025+SQL Server — full feature parity except spatial and hot Backup()
Existing MySQL estateMySQL — full feature parity except spatial
Existing Oracle estate, on Oracle 23ai+Oracle — full feature parity except spatial and native change feeds (vector / ANN search supported)
Embedded analytics, on-device aggregates, Parquet/CSV ingest alongside docsDuckDB — native JSON type, server-side json_merge_patch, columnar engine
Globally distributed, serverless / pay-per-RUCosmosDB — native spatial, automatic indexing, but watch RU cost on unindexed paths
Existing MongoDB estate / document-native workloadsMongoDB — typed BSON storage, full nested-field indexing via Mongo tooling, compensating transactions
Embedded, no native dependencies, Windows-friendlyLiteDB — client-side query evaluation, fine for small datasets
Blazor WebAssembly, client-only persistenceIndexedDB — zero native deps; if you need predicate push-down on large data, switch to SQLite-in-WASM