DocumentDB Releases
11.0.0 TBD
Section titled “11.0.0 TBD”Backup export/restore preserves CreatedAt / UpdatedAt (envelope v2). A backup round trip used to rewrite every document’s creation and modification timestamps to the import time. The export now records them and the Insert restore path binds them per row across every provider (relational + MongoDB/CosmosDB), so a backup faithfully preserves document history. The format is backward-compatible: a v1 backup (no timestamps) still imports, stamping the current time. DuckDB/PostgreSQL/SQL Server fall back from their native bulk-copy fast path to the multi-row insert only when timestamps are present.
Backup accounting and error shape. DocumentsWritten/DocumentsSkipped are now derived from row intent for Replace/Merge (MySQL’s ON DUPLICATE KEY UPDATE counts an update as two rows, which could make DocumentsSkipped negative). The native bulk-copy path now wraps a duplicate-key collision in the same friendly InvalidOperationException as the multi-row path instead of leaking the raw provider exception.
DynamoDB Streams change feed: no missed records across shard splits. A newly-split child shard was subscribed at LATEST, dropping any records written to it before the next shard refresh; child shards now start at TRIM_HORIZON and are not read until their parent shard has fully drained, preserving per-key ordering across a split. (Also removed a dead no-op branch in the poll loop.)
Scoped IDocumentInterceptor registration now fails fast. IDocumentStore is a singleton and resolves its DI-registered interceptors once from the root provider, so a Scoped interceptor would be a captive-dependency bug (a silently-reused singleton, or a scope-validation throw). AddDocumentStore now throws a clear error at registration directing you to register interceptors as Singleton (recommended) or Transient.
Generated serialization honors [JsonPropertyName] under trimming/AOT. A DocumentSerialization.Generated type with a [JsonPropertyName] on a queried property could emit the wrong JSON path once trimming removed the property’s reflection metadata, silently returning empty results. The generator now roots each document type’s public properties ([DynamicDependency]) so the query layer resolves the mapped name correctly after trimming.
IndexedDB inserts and version updates are now atomic. On Blazor WASM, Insert’s duplicate check and Update’s optimistic-concurrency check ran a JS get and put in separate transactions, so two overlapping writes could both pass their check. Both now run get-check-put inside a single readwrite transaction. (Upsert’s RFC 7396 deep-merge stays on the C# path.)
Vector search Score semantics documented as provider-specific. For Cosine/Euclidean the relational providers return a distance (lower = closer) while MongoDB/CosmosDB return a normalized similarity (higher = closer) — the same metric, opposite direction. There is no lossless canonical conversion, so results are always ordered nearest-first regardless of provider and VectorResult.Score is documented as backend-specific: rely on ordering, not the raw score, for portable ranking.
Enum queries work when enums are stored as strings. With a JsonStringEnumConverter (or a [JsonConverter] that emits strings) configured, enums are persisted as their member name — but the relational query pipeline always bound an enum == / != / in comparison as the underlying number and cast the extracted field to an integer, so every enum predicate silently matched nothing. The lowerer now detects a string-stored enum and binds the exact member-name string the write path persisted (extracting the field as text), across both the typed LINQ surface and the string grammar (Where("Level == 'High'")). Numeric-stored enums (the default) are unchanged. The fix spans the relational providers, CosmosDB (shared lowerer), and MongoDB (its own translator, fixed to match) for equality and in comparisons.
DuckDB bulk import no longer throws on a multi-tenant store. An Insert-mode BulkImport/Restore into a shared-table multi-tenancy DuckDB store threw a column-count mismatch (… has 6 columns but you specified only 5 values) because DuckDB’s native appender is positional and doesn’t account for the trailing TenantId column. DuckDB now falls back to the multi-row INSERT path when tenancy is enabled — leaving TenantId NULL, exactly like the PostgreSQL and SQL Server native bulk paths. (Tenant-scoped bulk import is still documented as unsupported; the imported rows are NULL-tenant — the fix is graceful parity rather than a hard failure.)
Mapped-property JSON names now honor [JsonPropertyName] and source-generated contexts. The version (MapVersionProperty), spatial, vector, computed, and full-text mappings resolved their stored JSON path from the runtime naming policy alone, which ignores the effective JSON name baked into a JsonTypeInfo. If a mapped property carried a [JsonPropertyName("…")] (or a source-gen context baked a different name), the resolver pointed at a key that isn’t in the document — most visibly, optimistic-concurrency writes on such a type failed with a phantom ConcurrencyException because the CAS predicate read a missing path. The mappings now read the effective name off the resolved JsonTypeInfo, falling back to the naming policy only when the type has no metadata.
Temporal history is now tenant-isolated. The _history sidecar gained a TenantId column and every temporal read/write (History / AsOf / AsOfAll / ChangesByActor / ChangesBetween / Restore / GetDiffBetween) is scoped to the current tenant. Previously a tenant could read another tenant’s version history via a known id, and AsOfAll returned every tenant’s documents. (Applies to newly-created history tables.)
Concurrent int/long auto-generated ids no longer collide. Auto-generated int/long ids come from SELECT MAX(Id)+1, which raced on pooled providers (two concurrent inserts picking the same id, one failing with a duplicate-key error). Insert now linear-probes the next free id on a collision, so concurrent inserts converge on distinct ids. Relatedly, a failed optimistic-concurrency Update now restores the caller’s in-memory version instead of leaving it bumped.
BulkWriteMode.Merge works on every relational provider, and change-feed robustness. RestoreAsync/BulkImportAsync with Mode = Merge no longer throws on PostgreSQL/MySQL/MariaDB/CockroachDB/SQL Server/Oracle — it falls back to a per-row merge (SQLite/DuckDB keep the native multi-row path). Change notifications on the LiteDB/Azure Table/DynamoDB stores are now buffered per async flow (AsyncLocal) so concurrent units of work and direct writes can’t cross-contaminate each other’s events. The PostgreSQL change-feed trigger is re-provisioned atomically (CREATE OR REPLACE TRIGGER, no drop window that loses events), a faulted change handler tears the subscription down instead of leaking into an unbounded queue, and the SQL Server poll bounds each read to the current snapshot version so a write racing the poll isn’t delivered twice.
Query correctness — LIKE escaping, numeric ordering, and != null semantics. Contains/StartsWith/EndsWith now escape %, _ (and [ on SQL Server) in the search term and emit an ESCAPE clause, so Contains("10%") matches the literal "10%" rather than any string starting with 10 — and the result now matches the in-memory providers’ literal String.Contains. OrderBy on a numeric property now extracts it as its typed value, so it sorts numerically (5, 25, 100) instead of lexicographically (100, 25, 5) on PostgreSQL/MySQL/SQL Server. A field != value predicate now includes rows where the field is null/absent (C# semantics), instead of dropping them via SQL three-valued logic. Projection sub-query constants (Count(pred)/Any(pred) inside a Select) are now normalized (dates/decimals/enums/guids) the same way top-level Where constants are. The OData $filter eq null/ne null against a non-nullable value-type property no longer throws.
String-grammar parity — pow, two-argument round, and concat. The string-expression surface (Where/OrderBy/Project) gained pow(x, y), round(x, digits) and concat(a, b, …), matching their LINQ (Math.Pow / Math.Round(x, n) / string +) equivalents.
Batch & unit-of-work writes now maintain the tenant column and all sidecars. BatchInsert previously used a fast path that skipped the TenantId column (making batch-inserted rows invisible and un-deletable per tenant) and the spatial R-tree index (making them un-findable by spatial queries). Batch inserts of tenant-scoped, spatial, vector, temporal, filtered or intercepted types now route through the per-document path, which handles all of that inside one transaction. Relatedly, unit-of-work writes (UnitOfWork / the atomic batch fallbacks) now maintain the spatial and vector sidecars (previously only relational + temporal), and a batch/UoW insert of a vector type runs its auto-embed hook. Clear<T> with a query filter now purges the spatial/vector index rows of the deleted documents, and ClearAll no longer errors on SQLite FTS5 shadow tables.
Vector search — DotProduct score direction and unsupported-metric gating. VectorResult.Score for the DotProduct metric is now the raw inner product (higher = closer) on every relational provider, as documented, rather than the database’s negated distance value (result ordering was already correct). Metrics a backend can’t express now fail loudly at mapping time instead of emitting SQL that errors on the server: DotProduct on SQLite (sqlite-vec supports only Cosine/Euclidean) and Hamming on PostgreSQL/CockroachDB (pgvector Hamming needs a bit(n) column) both throw NotSupportedException. MongoDB vector queries with a post-filter now widen the candidate pool so a filtered search doesn’t return fewer than k. Note MongoDB/CosmosDB report a similarity score (higher = closer) for all metrics, versus the relational providers’ distance score — the Score value’s meaning is provider-specific.
DocumentSerialization.Generated now diagnoses silent data loss. Types that the generated (AOT) metadata resolver can’t round-trip no longer serialize as an empty {} — the source generator now raises DDB005 for a property whose type is a collection other than List<T>/T[] (e.g. Dictionary, HashSet, ObservableCollection) and for an init-only or non-public-setter property that would be silently dropped. Fix the model (use List<T>/array, add a public setter, or [JsonIgnore]) or switch that type’s DocumentSerialization to Reflection/JsonContext/Auto.
Assorted robustness fixes. Cursor pagination raises the documented InvalidOperationException (not a raw FormatException) for a tampered cursor value; a malformed Lucene number (e.g. foo~1.2.3) raises the parser’s positioned error; a pure-negation Lucene query (-x) is now rejected in-memory too, matching the relational engines; RFC 6901 JSON-pointer escaping (~0/~1) is applied in GetDiff/patch-application; the RFC-7396 patch null no-op (not a key delete) is documented; the WKT writer closes polygon rings (SQL Server ingestion no longer rejects an unclosed ring); spatial envelopes handle antimeridian-crossing geometries; Covers no longer false-positives on a concave cover cutting an inner edge. Cosmos DB Orleans grain storage now percent-encodes the reserved / in grain ids (state was previously unreadable after a write). IndexedDB single-document writes resolve on transaction commit (surfacing quota/abort failures). The Cockroach soundex capability now fails loudly. Change-feed subscriptions no longer leak their CancellationTokenSource on a faulted handler, and normal stream cancellation is no longer recorded as a telemetry error.
Two new relational providers — MariaDB (Shiny.DocumentDb.MariaDb) and CockroachDB (Shiny.DocumentDb.CockroachDb) — both are thin, wire-compatible extensions of an existing provider, so they inherit the full document surface rather than re-implementing it. MariaDB extends the MySQL provider (same MySqlConnector driver): CRUD, LINQ-to-SQL, JSON indexes, batch/bulk, temporal history, computed columns, soundex and full-text all lower to identical dialect — new MariaDbDatabaseProvider("Server=…;Database=…;User=…;Password=…;"). It diverges in three documented places: spatial runs the portable envelope tier (bbox prune + in-process refine) because MariaDB’s ST_Distance is not metric for SRID-4326 geometry (a native distance query would return wrong metres); full-text drops the "a b"@N proximity operator (unimplemented in MariaDB boolean mode); and array-valued queries are unsupported — MariaDB has no JSON_TABLE (MDEV-16620) and no LATERAL, so predicates and projections that unnest a JSON array (Any/All over a collection, collection aggregates, GroupBy over an array element) throw a clear NotSupportedException at query-build time rather than emitting SQL that errors on the server. Scalar CRUD, filtering, ordering, projection, temporal, computed columns, full-text and backup all work as on MySQL. CockroachDB extends the PostgreSQL provider (same Npgsql driver): JSONB storage and operators, CRUD, ON-CONFLICT batch/upsert, read-merge-write patch, temporal, computed columns, partial JSON indexes and full-text search (tsvector + GIN + ts_rank, CockroachDB 23.1+) all work unchanged, and both spatial and vector search are native — PostGIS-compatible ST_* built-ins (no CREATE EXTENSION) and the pgvector-compatible VECTOR(n) type with the <->/<=>/<#> operators, so MapSpatialProperty and MapVectorProperty + NearestVectors both work unchanged (vector search runs brute-force — CockroachDB’s own CREATE VECTOR INDEX is v25.2+/L2-only). CockroachDB’s genuinely Postgres-only surfaces are scoped away: the LISTEN/NOTIFY change feed, native binary COPY bulk-copy, and soundex (fuzzystrmatch) report unsupported rather than emitting incompatible SQL — everything else, including bulk import via multi-row insert, works. Note CockroachDB’s SERIALIZABLE-by-default transactions may surface retryable (40001) errors under contention. Both providers are also wired into the Aspire integration (DocumentProviderKind.CockroachDb / .MariaDb — pass the kind explicitly to AsDocumentStore, since neither has a first-party Aspire hosting resource to auto-detect). See MariaDB and CockroachDB.
Cursor / keyset pagination — ToCursorPage / CursorPage<T> / ToCursorStream — a forward-only, seek-based alternative to offset paging that stays O(log n) per page (with an index over the sort key) and is stable under concurrent writes. store.Query<Order>().Where(o => o.Status == "open").OrderByDescending(o => o.CreatedAt).ToCursorPage(cursor, take: 50) returns a CursorPage<T> of Items plus an opaque NextCursor (null ⇒ last page); the keyset is derived from the query’s own OrderBy with an Id tiebreaker appended automatically so the order is always total. Pass null for the first page and hand the previous NextCursor back for each subsequent page — no ever-growing OFFSET, and no COUNT(*) round-trip. ToCursorStream(pageSize) walks every page as an IAsyncEnumerable<T> — a resumable full scan that never pays deep-offset cost. NULLs in an ordered column are handled correctly — they sort last (both ascending and descending, consistently across every provider) and the keyset seek carries across the NULL boundary rather than dropping the rest of the set. A cursor is valid only for the exact ordering that produced it (a shape hash over the OrderBy throws InvalidOperationException on the common “wrong sort” reuse), filters and sort must be stable across calls, and it is not valid after Select/Project/GroupBy (throws NotSupportedException); take must be > 0 and ≤ 10,000. Offset paging (Paginate / PageResult) stays for page-number UIs and totals — the two coexist and the docs steer you to the right one. Provider tier: keyset push-down on every relational provider (SQLite, SQLCipher, PostgreSQL, MySQL, SQL Server, Oracle, DuckDB); LiteDB, IndexedDB and MongoDB page the keyset client-side (same stable contract, without the server-side seek); Cosmos, DynamoDB and Azure Table don’t opt in yet and throw NotSupportedException (native continuation-token support is a follow-up). See Querying → Pagination.
Grouped aggregation — GroupBy / Having / IGroupedDocumentQuery — roll a filtered set up into one row per key, on top of the existing aggregate engine. store.Query<Order>().GroupBy(o => o.Status).Select(g => new StatusRollup { Status = g.Key, Count = g.Count(), Revenue = g.Sum(o => o.Total) }) groups on a JSON property, with g.Key for the group value and the Sql group aggregates g.Count() / g.Sum / g.Avg / g.Min / g.Max over the members. Aggregates extract their argument with the member’s CLR type, so Sum/Avg of a decimal keep full scale (no float round-off — exact on every provider with a native decimal type; SQLite aggregates as REAL) and Min/Max work over dates and strings, not just numbers. Keys may be nested (o => o.Address.Country), derived (o => o.CreatedAt.Month — a “by month” rollup with no stored column), or an anonymous type for a multi-column key (new { o.Status, o.Region } → g.Key.Status / g.Key.Region). Having(g => g.Sum(o => o.Total) > 10_000) filters groups by an aggregate (SQL HAVING); grouped results are ordinary rows, so OrderBy (over an output column), Paginate, Count (number of groups) and ToQueryString all flow through. String-grammar parity: GroupBy("status").Having("sum(total) > 10000").Project("status, count() as orders, sum(total) as revenue") lowers to the same SQL. The whole-set Count/Sum/Average terminals remain for ungrouped totals. Provider tier: push-down on the relational providers (SQLite, SQLCipher, PostgreSQL, MySQL, SQL Server, Oracle, DuckDB) — GROUP BY + HAVING + grouped ORDER BY + multi/derived keys; MongoDB, Cosmos, LiteDB and IndexedDB group client-side (the filtered set is read and aggregated in memory, like their Select), typed surface only; Azure Table and DynamoDB throw NotSupportedException (key-partitioned, no silent scan). See Querying → Grouping & aggregation.
Composable full-text with Lucene syntax — DocumentFunctions.LuceneMatch / LuceneScore — full-text as a composable predicate rather than a separate ranked call. DocumentFunctions.LuceneMatch(a.Body, "orleans AND grain NOT deprecated") goes inside a Where (AND/OR with ordinary predicates, paging, etc.) and DocumentFunctions.LuceneScore(a.Body, "orleans grain") inside an OrderBy/projection, both translating a Lucene query — terms, "phrases", AND/OR/NOT (&&/||/!, +/-), ( grouping ), prefix foo*, fuzzy foo~, proximity "a b"~5, boost foo^2 — to the provider’s native full-text engine over the existing MapFullTextProperty index (the type must still be mapped). Also in the string grammar: Where("lucenematch(body, 'title:quick AND brown~')"), OrderBy("lucenescore(body, 'orleans') desc"), interpolated Where($"lucenematch(body, {q})"). Provider tier: composable match+score on SQLite (FTS5), PostgreSQL (tsquery), MySQL (BOOLEAN MODE), SQL Server (CONTAINS); Oracle Text is match-only (SCORE(n) needs a co-located label — use FullTextSearch for ranking); LiteDB/IndexedDB run the full grammar in-memory (including fuzzy). DuckDB, CosmosDB and MongoDB don’t support composable queries (their engines can’t express the boolean grammar inline) — use store.FullTextSearch(...) there. Operators a backend can’t express throw NotSupportedException (never silent degradation); field-scoped terms (title:foo) are not supported in v1 on any provider. See Full-Text Search.
OpenTelemetry instrumentation for keyed / named stores — AddDocumentStoreInstrumentation gained a AddDocumentStoreInstrumentation(string name) overload that decorates the keyed IDocumentStore registered under name (the AddDocumentStore(name, …) overload), and DocumentStoreOptions.Instrumentation = true now works on the eager keyed overload AddDocumentStore(name, Action<DocumentStoreOptions>) instead of throwing NotSupportedException. Metrics and spans from a named store carry an extra db.namespace tag (the store name) so signals from multiple stores are distinguishable; the non-keyed path is byte-for-byte unchanged (tag omitted). The keyed decorator is idempotent and faithfully re-exposes ITemporalDocumentStore/IObservableDocumentStore/IChangeFeedDocumentStore, so casts keep working. The lazy keyed overload AddDocumentStore(name, Action<IServiceProvider, DocumentStoreOptions>) configures options at resolve time, so instrument it with an explicit AddDocumentStoreInstrumentation(name) call. See Telemetry & Diagnostics.
Merge-vs-replace flags on Update and Upsert — pick the write mode explicitly without switching methods. Update(doc, patch: false) is the existing full replace; Update(doc, patch: true) RFC 7396 deep-merges into the existing document (must already exist). Upsert(doc, patchIfUpdate: true) is the existing merge-or-insert; Upsert(doc, patchIfUpdate: false) replaces the body wholesale on update and inserts if absent. Same flags on the late-bound JSON lane — Update(type, jsonObject, patch: true) / Upsert(type, jsonObject, patchIfUpdate: false) — which is the precise way to do partial updates (a typed object always serializes every property, so patch: true on it only skips null fields; a partial JsonObject changes exactly the keys it carries). The merge modes strip null properties (unset fields are left unchanged, never deleted — a null in a patch is a no-op, not an RFC 7396 key deletion). The opt-in merge/replace modes read-modify-write inside a transaction with a row-locking SELECT (FOR UPDATE / UPDLOCK) on every relational provider, so concurrent partial writes to the same document don’t lose updates. Provider tier: the flags are implemented on the relational providers (SQLite, SQLCipher, MySQL, SQL Server, PostgreSQL, Oracle, DuckDB) and the JSON lane; the two default behaviours (Update replace, Upsert merge) work on every store, while the non-default modes throw NotSupportedException on the document-native and key-partitioned stores. See CRUD.
Spatial predicates in LINQ via DocumentFunctions — compose a spatial predicate with the rest of a query (other Where clauses, OrderBy, Count, paging), all server-side: store.Query<Zone>().Where(z => DocumentFunctions.Intersects(z.Area!, area) && z.Active).OrderBy(z => DocumentFunctions.Distance(z.Area!, origin)). The family — Intersects, Disjoint, Contains, Within, Covers, CoveredBy, Touches, Crosses, Overlaps, GeoEquals, WithinDistance (in Where) and Distance (in OrderBy) — lowers to each engine’s native spatial function or a registered UDF. Every predicate lowers to the engine’s native spatial function (or a UDF on SQLite), backed by a real 2-D spatial index: SQLite R*Tree + docdb_st_* C# UDF (dependency-free); PostgreSQL GiST, MySQL SPATIAL, DuckDB R-Tree, SQL Server spatial index, Oracle SDO_GEOMETRY column + MDSYS spatial index (SDO_RELATE operators, requires Oracle Spatial) — each over a native geometry column in the sidecar populated on write (PostgreSQL needs PostGIS, DuckDB auto-loads spatial); CosmosDB ST_INTERSECTS/ST_WITHIN/ST_DISTANCE; MongoDB $geoIntersects/$geoWithin (2dsphere index, ensured on the LINQ path). CosmosDB/MongoDB expose the intersect/within/distance subset in a Where; the finer predicates throw there and are served by the dedicated store.Geo* methods (which support every predicate on every spatial-capable provider). WithinDistance in a Where needs a geodesic (metric) distance function, which SQL Server (planar geometry sidecar) and DuckDB (no polygon geodesic distance) don’t have — rather than silently approximating with planar degrees (wrong away from the equator), they throw NotSupportedException; use store.GeoWithinDistance(...), which refines with an exact Haversine distance in managed code and is correct on every provider. Set PortableSpatial = true on a relational provider to force the dependency-free envelope tier. The same geo functions also work in the string-expression surface — Where("…"), interpolated Where($"…"), OrderBy("…"), Project("…") — with the query geometry supplied as an interpolated {value} (a bound Geometry/GeoPoint) or an inline GeoJSON string literal. See Spatial.
Full geometry spatial queries — the spatial feature grows from point-only to full OGC geometry. A new Geometry model (GeoLineString, GeoPolygon with holes, GeoMultiPoint, GeoMultiLineString, GeoMultiPolygon, GeoGeometryCollection; GeoPoint implicitly converts to a point geometry) serializes as GeoJSON in the document body, and MapSpatialProperty<T>(x => x.Area) now accepts a Geometry? property alongside the existing GeoPoint? overload. Query it with the Geo-prefixed topological predicate family — GeoIntersects, GeoContainedBy, GeoContains, GeoDisjoint, GeoTouches, GeoCrosses, GeoOverlaps, GeoEquals, GeoCovers, GeoCoveredBy — plus GeoWithinDistance(geometry, meters) for a distance band. Every predicate takes an optional orderByDistanceFrom (point or geometry) and returns SpatialResult<T> with DistanceMeters populated; NearestNeighbors now works over geometry-mapped types too. The Geometry model also exposes in-memory Area/Length/Perimeter/Centroid/NumPoints/NumGeometries (C# accessors — to filter by a measurement server-side, compute the scalar in your app and store it as a normal indexed field) and IsValid/IsSimple/MakeValid. Provider coverage — every SQL provider plus the document stores: SQLite via the R*Tree bbox prune + in-process relate/refine; PostgreSQL, MySQL, SQL Server, Oracle, and DuckDB via a dependency-free envelope-sidecar table (four indexed columns) + the same in-process refine — no PostGIS/geography/SDO_GEOMETRY/DuckDB-spatial extension required; CosmosDB pushes ST_INTERSECTS/ST_WITHIN/ST_DISTANCE down natively; MongoDB gains spatial via a 2dsphere index with native $geoIntersects/$geoWithin/$near. On Cosmos/Mongo the finer predicates refine over the intersect candidate set. GeoDisjoint is anti-selective and scans the type (O(n)) on the two-pass providers. The remaining fallback stores (LiteDB, IndexedDB, Azure Table, DynamoDB) stay SupportsSpatial => false. Existing GeoPoint mappings and WithinRadius/WithinBoundingBox/NearestNeighbors are unchanged. See Spatial.
10.0.2 July 3, 2026
Section titled “10.0.2 July 3, 2026”Nullable spatial locations no longer throw — MapSpatialProperty<T> now accepts a nullable GeoPoint? property (both the expression and the AOT-safe (propertyName, accessor) overloads), and a document whose mapped location is null is skipped by the spatial index instead of throwing. Previously, inserting or updating a document with a null location threw a NullReferenceException on the write path (the R*Tree sync called the accessor unconditionally) — the common case for records where coordinates are optional (e.g. a calendar event that may not have a place). Setting a previously-populated location back to null on update now also purges the stale index row, so the document stops matching WithinRadius/WithinBoundingBox/NearestNeighbors. The write-path crash affected the relational R*Tree provider (SQLite); the mapping signature and read-path guards are applied to CosmosDB for consistency. Existing non-nullable GeoPoint mappings compile unchanged. See Spatial.
10.0.1 - July 3, 2026
Section titled “10.0.1 - July 3, 2026”Instrumentation = true DI flag for OpenTelemetry — Shiny.DocumentDb.Extensions.DependencyInjection now references the Diagnostics decorator, so you can opt into metrics + tracing with a single option instead of a separate AddDocumentStoreInstrumentation() call: services.AddDocumentStore(o => { o.DatabaseProvider = …; o.Instrumentation = true; }). It’s equivalent to calling the extension after registration and still requires you to subscribe your OTel pipeline to the Shiny.DocumentDb meter/source. Honored by the non-keyed AddDocumentStore overloads (including multi-tenant); setting it on a keyed/named store throws NotSupportedException, since the decorator only wraps the non-keyed registration. Registering through the DI package now pulls the (lightweight, in-box) diagnostics dependencies; bare new DocumentStore(options) usage stays dependency-free. See Telemetry & Diagnostics.
10.0 - July 2, 2026
Section titled “10.0 - July 2, 2026”Late-bound JSON lane (Type + JsonNode) — write and read documents by handing the store a registered document Type and the JSON body directly, without a CLR T. The exact tool for generic HTTP intake, message-bus payloads, ETL, and gateways where you hold the payload as JSON. Writes: store.Insert(type, node) / Update / Upsert accept a JsonObject (one document) or a JsonArray (many, written atomically in one transaction — a mid-batch failure rolls the whole call back) and return the number written; a primitive JsonValue throws. The body is stored AS-IS (property names must match the type’s serialized shape, camelCase by default), so serialization is skipped entirely. Unlike IDocumentBackup/BulkImport, this lane rides the full normal write pipeline — tenancy, temporal history, versioning/optimistic-CAS, spatial + vector sidecars, JSON interceptors, and change notifications all apply; the generated Id (and bumped version) is injected back onto your node exactly like the typed Insert<T>. Because the body is verbatim, every registered spatial/vector mapping’s JSON path must be present — an absent path throws InvalidOperationException naming it, while an explicit JSON null is honored as a deliberate “no value” (the sidecar is skipped). Upsert runs that presence check only when the element carries no Id (a guaranteed insert), so partial RFC 7396 merge patches aren’t forced to re-send the location/vector. Reads: store.Get(type, id) returns the raw document as a JsonNode?, and store.Query(type, whereClause, parameters) / QueryStream(...) run the same string WHERE surface as Query<T>(string) but hand back JsonNodes with no deserialize to T — cheaper than the typed read and AOT-clean (no JsonTypeInfo<T> needed at all). Filtering reuses the existing WHERE/OData string; there is deliberately no “filter by JsonNode”. Two documented limitations: object-mutating interceptors are a no-op on this lane (there is no T to mutate — JSON-shaped interceptors via ctx.GetJsonDocument() still work fully), and vector auto-embedding does not run (supply the embedding in the JSON). Provider tier: supported on the relational providers (SQLite, SQLCipher, MySQL, SQL Server, PostgreSQL, Oracle, DuckDB) via the core store; the document-native (MongoDB, Cosmos, LiteDB, IndexedDB) and key-partitioned (Azure Table, DynamoDB) providers throw NotSupportedException until a later cut, and the lane is not available inside a UnitOfWork. See CRUD.
Azure Table Storage provider (Shiny.DocumentDb.AzureTable) — a schema-free document store over Azure Table Storage (and the Cosmos DB Table API, same SDK) built on Azure.Data.Tables. It’s a NoSQL key-partitioned store: the library’s (typeName, id) identity maps to PartitionKey = typeName / RowKey = id, one table holds every type, and Query<T>() is always a single-partition read. Register with services.AddAzureTableDocumentStore(o => { o.ConnectionString = …; o.TableName = "Documents"; }) (connection string, SAS, shared key, or ServiceUri + DefaultAzureCredential/TokenCredential) — it wires IDocumentStore + IDocumentMaintenance. Full CRUD, batch (native SubmitTransaction in ≤100-item waves per partition), RFC 7396 merge Upsert, typed Query<T>(), Count/Clear/Remove, compensating CreateUnitOfWork(), and ETag-backed optimistic concurrency via MapVersionProperty<T> (ConcurrencyException on conflict). Rich LINQ queries evaluate client-side (the LiteDB ExpressionInterpreter model) after loading the type’s partition. Promote hot query paths with MapIndexedProperty<T>(x => x.Status) — the scalar is written as a native top-level column and LINQ predicates over it (plus the OData string Query/QueryStream/Count overloads and ToQueryString) push down into a server-side $filter to shrink the candidate set (the full predicate still re-runs client-side, so results are always exact). In-process change observation ships too — IObservableDocumentStore.NotifyOnChange<T> and Query<T>().NotifyOnChange(). Int/Long Id auto-generation is unsupported (no cheap MAX — use Guid/string, or assign explicitly); no spatial/vector/full-text/temporal. A body over the ~64 KB per-property cap throws a clear NotSupportedException. See Azure Table Storage.
Amazon DynamoDB provider (Shiny.DocumentDb.DynamoDb) — a schema-free document store over DynamoDB built on AWSSDK.DynamoDBv2. Same NoSQL key-partitioned model: (typeName, id) maps to partition key pk = typeName (HASH) and sort key sk = id (RANGE) in one table. Register with services.AddDynamoDbDocumentStore(o => { o.TableName = "Documents"; o.Region = RegionEndpoint.USEast1; }) (standard AWS credential chain, explicit Credentials, or ServiceUrl for DynamoDB Local; AutoCreateTable for dev; ConsistentRead toggles strongly-consistent reads) — it wires IDocumentStore + IDocumentMaintenance. Full CRUD, batch (native BatchWriteItem in ≤25-item waves with UnprocessedItems retry), RFC 7396 merge Upsert, typed Query<T>(), Count/Clear/Remove, compensating CreateUnitOfWork(), and conditional-write optimistic concurrency via MapVersionProperty<T> (a top-level Version attribute guard → ConcurrencyException). Same client-side query tier as Azure Table with the same MapIndexedProperty<T> promotion — predicates over promoted attributes push down into a FilterExpression, and the string Query/QueryStream/Count/ToQueryString overloads speak PartiQL over them. Ships both change surfaces: in-process IObservableDocumentStore.NotifyOnChange<T> and a native DynamoDB Streams–backed IChangeFeedDocumentStore.SubscribeChanges<T> (the table’s stream is enabled automatically on create) that observes inserts/updates/deletes from any writer. Same limitations as Azure Table (Int/Long auto-gen unsupported; no spatial/vector/full-text/temporal; 400 KB item cap throws a clear error). See Amazon DynamoDB.
Strongly-typed DocumentContext — an optional, EF-Core-style typed front-end over IDocumentStore. Declare your aggregates once on a partial context with [Document(typeof(User), Id = nameof(User.Email), JsonContext = typeof(AppJsonContext))], and the source generator bundled inside Shiny.DocumentDb (an analyzer under analyzers/dotnet/cs — no separate package to install) source-generates a DocumentSet<T> property per type, a ConfigureModel lowering (table/id mappings + JSON resolver wiring), and two DI extensions: services.AddAppContext(...) registers the context scoped (ASP.NET Core request scopes), and services.AddAppContextFactory(...) registers a singleton IDocumentContextFactory<AppContext> whose Create() news up a short-lived context on demand — the MAUI / Blazor / desktop story, mirroring EF Core’s AddDbContextFactory/IDbContextFactory<T> for apps with no ambient DI scope (inject the factory anywhere, even into singletons). Each context’s store is now keyed by the context type, so multiple DocumentContext types coexist in one container without shadowing each other on a single IDocumentStore (the first registered context stays the default un-keyed IDocumentStore for the extension packages that inject it). You then work model-first — await db.Users.Where(u => u.Age >= 18).ToList(), await db.Users.Insert(user) — with the JsonTypeInfo<T> threaded automatically, so you never re-type <T> or pass type metadata. DocumentContext/DocumentSet<T> ship in core and only need an IDocumentStore, so they work over all providers; the generated ConfigureModel/DI sugar targets the relational DocumentStoreOptions (for LiteDB/Mongo/Cosmos, build that store yourself and pass it to the context). It’s an ergonomics + discoverability layer only — no change tracking, identity map, or navigation/Include; writes are immediate (group them with CreateUnitOfWork()). Serialization is a per-type knob via [Document(..., Serialization = ...)]: JsonContext (point at your JsonSerializerContext — recommended, AOT-safe), Auto (inherit the store’s resolver, else reflection fallback), Reflection (explicit non-AOT opt-out), and Generated — where the generator emits the metadata-mode JsonTypeInfo (and its whole reachable type closure) itself via JsonMetadataServices, giving AOT-safe serialization from the single [Document] declaration with no hand-written JsonSerializerContext. Generated supports POCOs with a parameterless constructor and settable public properties of JSON primitives, Guid/date-time types, enums, nullable value types, nested objects, List<T>, and arrays (honoring [JsonPropertyName]/[JsonIgnore]); types outside that subset (records, parameterized/init-only constructors, dictionaries) raise DDB005 directing you to JsonContext. The generator reports clear diagnostics for misuse (DDB001 not partial, DDB002 doesn’t derive DocumentContext, DDB003 duplicate set name, DDB005 unsupported Generated type). See Typed Context.
Computed properties — map a value derived from other fields (Total = Quantity * UnitPrice, FullName = First + " " + Last, a normalized lower(Email)) that you filter, sort, and project by exactly like a stored property, even though it’s never written into the document JSON. Register it with MapComputedProperty<T>(o => o.Total, o => o.Quantity * o.UnitPrice) — the first expression is the [JsonIgnore] property it backs, the second is the definition — then reference it by name in typed LINQ (Where(o => o.Total > 100)), the string API (Where("total > 100").OrderBy("fullName")), Project("fullName as name, total"), and OData ($filter/$orderby/$select). By default it runs in alias mode: the definition is translated to SQL and inlined wherever the property appears — zero schema changes, every relational provider. Pass indexed: true and the relational providers materialize it as a native generated/computed column + index so filters/sorts are index-served — VIRTUAL generated column (SQLite, MySQL), STORED generated column (PostgreSQL), PERSISTED computed column (SQL Server), virtual column (Oracle) — engine-maintained, so it stays correct through every write (DuckDB can’t add a generated column via ALTER, so it uses alias mode). The value is recomputed and written back onto the object on read, so a round-tripped document is complete despite never being stored. LiteDB and IndexedDB evaluate it in memory (full filter/sort/project/read-back client-side); MongoDB and Cosmos support read-back and projection (server-side filter/sort by a computed property is not translated — filter on the underlying stored fields there). Definitions support JSON field access, string concatenation, the scalar functions, and numeric arithmetic (+ - * /, newly added to the query pipeline and available in ordinary Where clauses too). Fully Native-AOT/trim-safe — the definition is tree-walked to SQL and interpreted for read-back, never compiled.
Streaming bulk export / import / restore (IDocumentBackup) — move a whole store’s contents in and out as a portable, streamed v1 backup document. It’s a separate store capability (probe with store is IDocumentBackup, like IDocumentMaintenance — not on IDocumentStore), implemented by the relational DocumentStore (every SQL provider), MongoDB, and Cosmos DB. Three methods: ExportAsync(Stream, BackupExportOptions?) writes the store out as a JSON array of { id, docType, data } records (the document body emitted as-is; DocTypes filters which types, Indented pretty-prints); RestoreAsync(Stream, BulkRestoreOptions?) streams a backup back in with a forward-only reader so a multi-GB file never lands fully in memory; and the lower-level BulkImportAsync(IAsyncEnumerable<RawDocument>, …) feeds pre-shaped raw rows (RawDocument(string Id, string DocType, ReadOnlyMemory<byte> Data)) from any source — RestoreAsync is just the JSON adapter over it. Bodies are bound verbatim: no <T>, no JsonTypeInfo, no reflection over the documents (AOT-friendly). BulkWriteMode picks the collision strategy — Insert (fail on duplicate Id; fastest; multi-row VALUES everywhere, native bulk copy where available), Replace (overwrite the body wholesale), Merge (RFC 7396 deep-merge, same semantics as BatchUpsert), and SkipExisting (insert new, silently skip existing). BulkRestoreOptions adds ClearExistingFirst, ChunkSize (default 500), SingleTransaction (false = commit per chunk: resumable, bounded WAL/log), and an IProgress<BulkProgress> callback; the result is BulkRestoreResult(DocumentsRead, DocumentsWritten, DocumentsSkipped, ChunksCommitted). The import path deliberately skips versioning/CAS, temporal history, interceptors, tenant scoping, and global query filters — that’s where the speed comes from, so treat it as a raw restore lane, not a replacement for BatchUpsert. Provider tiers: Insert works on every provider; Replace & SkipExisting on all relational providers (ON CONFLICT on SQLite/DuckDB/PostgreSQL, ON DUPLICATE KEY/INSERT IGNORE on MySQL, MERGE on SQL Server & Oracle) plus Mongo/Cosmos; Merge only on SQLite, DuckDB and Mongo/Cosmos (it throws NotSupportedException on PostgreSQL/MySQL/SQL Server/Oracle — use Replace there). A native bulk-copy fast path makes Insert 10-100× faster on PostgreSQL (binary COPY), SQL Server (SqlBulkCopy), and DuckDB (appender). Caveats: Mongo/Cosmos imports are best-effort, not atomic (SingleTransaction is ignored); Oracle Replace/SkipExisting build the MERGE source via SELECT … FROM DUAL UNION ALL, which can reject documents above the VARCHAR2 bind limit (bound as CLOB); Cosmos export covers the whole database (all containers) while relational export covers the store’s configured tables. For a full restore prefer Insert or Replace — under Merge, a null in a body deletes that field (RFC 7396).
9.1 - June 24, 2026
Section titled “9.1 - June 24, 2026”Full-text search across every provider — find documents by relevance, not substring. Register a searchable property with MapFullTextProperty<T>(a => a.Body) (or several fields combined, [a => a.Title, a => a.Body]), then query with store.FullTextSearch<T>("orleans persistence") or the fluent store.Query<T>().Where(...).FullTextMatch("..."). Results come back ranked — IReadOnlyList<FullTextResult<T>> with a Document and a normalized Score (higher = more relevant) — with an optional pre-filter predicate for tenant/category scoping. Each backend runs its native engine and the library creates the index for you at startup: FTS5 (SQLite), generated tsvector + GIN (PostgreSQL), generated column + FULLTEXT (MySQL), Oracle Text CTXSYS.CONTEXT, Full-Text Index + FREETEXTTABLE (SQL Server), the fts extension (DuckDB), full-text policy + FullTextScore (Cosmos DB), and a $text index (MongoDB); LiteDB and IndexedDB fall back to an in-memory TF-IDF scan so the API works everywhere. The index is engine-maintained — Insert/Update/Remove/Clear keep it in sync with no write-path bookkeeping. Full-text is declarative: a type must be mapped before it can be searched (that’s what lets the index be provisioned), and engines with a single full-text index per table (SQL Server, MongoDB) support one mapped type per table/collection. Oracle Text and SQL Server Full-Text Search are optional server components that must be installed for those providers. Cosmos full-text requires Microsoft.Azure.Cosmos 3.61.0+ (the provider’s pinned version was bumped accordingly).
9.0 - June 22, 2026
Section titled “9.0 - June 22, 2026”OData governance — result caps, allowlists & complexity limits — Shiny.DocumentDb.AspNetCore.OData endpoints can now be locked down with an ODataQueryPolicy per entity set, the security surface a public OData API needs. Set API-wide defaults with ConfigureDefaultPolicy(...) and override per set via the new EntitySet<T>(name, policy => …) overload (the override clones the defaults). Controls: DefaultPageSize (applied when $top is omitted, so reads are never unbounded) and MaxTop/MaxSkip; MaxFilterNodeCount/MaxOrderByNodeCount to reject pathologically complex queries; AllowFilter/AllowOrderBy/AllowSelect/AllowCount and AllowArithmetic to disable whole options; an AllowedFunctions allowlist; and FilterableProperties/SortableProperties/SelectableProperties per-property allowlists (matched on the root path segment). A disallowed-but-well-formed request is rejected with 400 and a message naming the offender; the effective page size is clamped to MaxTop. Defaults are fully permissive, so existing endpoints are unchanged until you opt in. The ODataQueryPolicy type ships in the dependency-free engine package, so the same limits apply to any non-HTTP caller of the engine.
OData unknown-property queries return 400 (not 501) — a $filter/$orderby/$select that names a property not on the entity (e.g. $select=Bogus) is bad client input and now returns 400 Bad Request. Previously the Microsoft parser’s ODataException was mapped to 501 along with $expand; only $expand (no document relationships) remains a 501.
SQLite decimal filters — decimal comparison constants in .Where(...) / OData $filter (e.g. Price gt 100, Balance eq 49.00) now bind correctly on SQLite. Microsoft.Data.Sqlite binds a CLR decimal as TEXT, and SQLite type affinity ranks TEXT above REAL, so a numeric JSON value never matched a decimal parameter and these predicates silently returned nothing. The SQLite provider now binds decimals as REAL. Integer, double, string, and boolean filters were unaffected; other providers bind decimals natively and were never impacted.
OData $expand returns 501 (not 500) — $expand on the ASP.NET Core OData host now reliably returns 501 Not Implemented as documented. Because documents have no navigation properties, the Microsoft parser raises an ODataException while binding the expand clause, which previously surfaced as a 500. The host now catches it and returns 501.
Aspire client — container-aware store configuration & one-flag multi-tenancy — the consuming-service AddDocumentStore(...) gains a configureServiceOptions: (sp, o) => … callback that runs with the resolved IServiceProvider, so options that depend on other registered services (e.g. resolving an interceptor or a custom tenant accessor from the container) can finally be wired through the Aspire client — the existing configureOptions (JSON contexts, type/table maps, query filters, interceptor instances) is unchanged. A new DocumentStoreSettings.MultiTenant flag registers a shared-table multi-tenant keyed store in one line: it adds the TenantId column, filters every query by the current tenant, and resolves it from a registered ITenantResolver. The same capability is now available to non-Aspire callers on the core DI package: AddDocumentStore(services, name, configure, multiTenant: true) registers a keyed shared-table tenant store (the keyed equivalent of the existing non-keyed multiTenant overload), and the lower-level AddDocumentStore(services, name, Action<IServiceProvider, DocumentStoreOptions>) is public for any DI-aware configuration of a named store.
Offline-first sync (Shiny.DocumentDb.AppDataSync) — make IDocumentStore the local cache of a backend that bidirectionally syncs over HTTP via Shiny.Data.Sync. Register AddDocumentStore(...) + AddDataSync<TDelegate>(...) + SyncDocumentStore(sync => sync.Sync<TodoItem>()), and an ordinary document type becomes two-way synced with no manual Queue/delegate plumbing: local Insert/Update/Upsert/Remove auto-enqueue to the reliable, background-capable outbox, and pulled server changes auto-apply back into the store (Create/Update → Upsert, Delete → Remove). Inbound applies run through SaveChanges(suppressInterceptors: true), so they never echo back to the server (loop guard) and fire no other interceptor. Synced types implement Shiny.Data.Sync.ISyncEntity (its string Identifier, conventionally Id.ToString()); the store and sync serializers are validated to share one JSON contract at startup. Set-based writes (ExecuteUpdate/ExecuteDelete/Clear<T>) throw SyncBulkWriteNotSupportedException on synced types (use ClearAll for a whole-store reset); batch writes enqueue each item. Client-tier providers (SQLite, LiteDB, IndexedDB).
OData v4 query endpoints (Shiny.DocumentDb.OData + Shiny.DocumentDb.AspNetCore.OData) — expose a document type as an OData entity set. $filter/$orderby/$top/$skip/$count/$select translate onto the fluent IDocumentQuery<T> and run against any provider; $expand returns 501 (no relationships). AddDocumentODataEndpoints(edm => edm.EntitySet<Customer>("customers")) + app.MapDocumentODataEntitySet<Customer>("odata/customers"). The translator engine is dependency-free and AOT/trim-clean; the ASP.NET Core host (Microsoft.AspNetCore.OData for full EDM/$metadata compliance) is JIT-only. Global AddQueryFilter predicates always apply underneath the translated $filter, so a client can’t filter past them; $count is computed pre-paging.
JSON Schema validation before write (Shiny.DocumentDb.JsonSchema) — attach a JSON Schema (draft 2020-12, powered by JsonSchema.Net) to a document type and the store validates the exact JSON about to be persisted just before the write. A failure throws DocumentSchemaValidationException (with field-level Errors) and rolls the write back — the only structural contract a schema-free store can offer. Register via DI (services.AddDocumentJsonSchema(o => o.MapJsonSchema<Customer>(schemaJson))) or inline in options (options.AddJsonSchemaValidation(...)); map schemas by JsonSchema object, JSON text, Stream, or MapJsonSchemaFromFile<T>(path) (all parsed once at registration). format (email/uuid/date-time) is asserted by default (EnableFormatAssertion = false for annotation-only). Validate what the C# type can’t — maxLength, ranges, pattern, enum, additionalProperties:false, reference-type required-ness. Schema property names match the serialized (camelCase) JSON names. Works on every provider (rides the shared interceptor path); deletes and set-based writes pass through.
services.AddDocumentJsonSchema(o => o.MapJsonSchema<Customer>("""{ "type":"object", "required":["name","email"], "properties": { "name": { "type":"string", "minLength":1, "maxLength":100 }, "email": { "type":"string", "format":"email" } } }"""));// store.Insert(invalidCustomer) -> throws DocumentSchemaValidationException, nothing persistedSide-effect-free writes & the serialized-JSON write context — two additions to the core write path:
UnitOfWork.SaveChanges(suppressInterceptors: true)commits a unit with no interceptor (per-document or bulk) firing — bounded by that transaction, so writes outside the unit still fire normally. The right tool for mirrored / authoritative data that should carry no side effects: bulk import, seeding, migration, and the inbound apply ofShiny.DocumentDb.AppDataSync. While suppressed, the multi-row batch fast path is re-enabled.- Inside
IDocumentInterceptor.BeforeWrite,ctx.GetJson()/ctx.GetJsonDocument()expose the exact JSON about to be persisted (the store’s own options/JsonTypeInfo, cached, invalidated if an earlier interceptor replaces the document). Useful for auditing/redaction; it’s the primitive the JSON-Schema package builds on.
Batch upsert, update & remove — BatchUpsert<T>, BatchUpdate<T> and BatchRemove<T> join BatchInsert<T> on IDocumentStore, collapsing many single-document round-trips into one set operation. They are all-or-nothing: on a versioned type the first version conflict throws ConcurrencyException and the whole batch rolls back. The win varies by backend (provider tier):
- SQLite & DuckDB —
BatchUpsertemits a single multi-rowINSERT … ON CONFLICT … DO UPDATEthat deep-merges (RFC 7396) every row in one statement. - MongoDB —
BatchUpsert/BatchUpdateuse oneBulkWrite;BatchRemoveuses oneDeleteMany. - Cosmos — upsert/update/remove run as bounded-concurrency waves (parallel requests parallelize RU spend); same-type batches share one partition key.
- All relational —
BatchRemoveissues a singleDELETE … WHERE Id IN (…);BatchUpdateloops per-row inside one transaction (heterogeneous-value updates have no clean multi-row form). - Versioned, temporal, spatial, vector, multi-tenant, filtered, or interceptor-bound types take a per-document loop inside one transaction — correct and atomic, just not the multi-row/bulk fast path.
UnitOfWork now coalesces contiguous same-type runs of upserts, updates, and removes (not just inserts) into the matching batch method, so grouping like operations in a unit costs nothing versus calling the batch methods directly.
await store.BatchUpsert(users); // one multi-row statement on SQLite/DuckDBawait store.BatchRemove<User>(new object[] { 1, 2, 3 }); // one DELETE … IN (…)
await store.CreateUnitOfWork() .Upsert(a).Upsert(b).Upsert(c) // → one BatchUpsert .Remove<User>(staleId) // → one BatchRemove .SaveChanges();Cosmos bulk deletes — Clear<T>() and Query<T>().ExecuteDelete() on the Cosmos provider previously deleted matched rows one HTTP request at a time. They now issue the deletes in bounded-concurrency waves, cutting wall-clock on large clears dramatically.
ClearAll() deleted system-catalog tables on PostgreSQL and MySQL — IDocumentMaintenance.ClearAll() lists tables via information_schema.tables, which surfaces system tables too. On PostgreSQL those system catalogs are reported as BASE TABLE, so ClearAll issued DELETE against pg_catalog/information_schema — corrupting the connection’s catalog cache (cache lookup failed for type …) and, with pooled connections, poisoning later operations on the reused connection (type "text" does not exist). On MySQL, whose information_schema is server-wide, it listed every schema’s tables (Table 'processlist' doesn't exist). The default table listing now excludes the pg_catalog/information_schema system schemas, and the MySQL provider additionally scopes to the current database (TABLE_SCHEMA = DATABASE()). SQLite, Oracle, SQL Server, and DuckDB were unaffected.
8.1.1 - June 21, 2026
Section titled “8.1.1 - June 21, 2026”Inspect the generated query with IDocumentQuery<T>.ToQueryString() — see the SQL (or MongoDB BSON) a query would run, without executing it — for debugging, logging, and learning how an expression translates. Works for both the LINQ and string-expression Where forms (they share one pipeline). Returns a DocumentQueryString exposing Sql and a Parameters name→value map; its ToString() renders the values as a comment header above the query for copy/paste. Reflects the ToList() form including Where/OrderBy/Paginate/Select/Project. Relational providers (SQLite, SQL Server, PostgreSQL, MySQL, Oracle, DuckDB) and Cosmos return their query text + parameters; MongoDB returns its rendered BSON filter (or full find command). The in-memory providers (LiteDB, IndexedDB) — and client-side projections after Select/Project on the document providers — throw NotSupportedException.
var qs = store.Query<User>().Where(u => u.Age > 28).ToQueryString();Console.WriteLine(qs);// -- @typeName='User'// -- @p0=28// SELECT Data FROM "documents" WHERE TypeName = @typeName AND (json_extract(Data, '$.age') > @p0);8.1 - June 21, 2026
Section titled “8.1 - June 21, 2026”Document seeding — register IDocumentSeeders to populate initial data once at startup. Because the store is schema-free, seeding is just idempotent writes — so seeders are provider-agnostic and work against every backend. Run-once semantics are versioned via a DocumentSeedMarker document keyed on the seeder name: a seeder runs when it has never run or when its Version is greater than the recorded one — bump the version to re-run after changing seed data. Register with AddDocumentSeeder<T>() / AddDocumentSeeder(name, version, delegate) (executed at host startup via a hosted service) — pass storeName to seed a named/keyed store — or call DocumentSeedRunner.RunAsync(store, seeders) directly where there’s no generic host (e.g. MAUI).
builder.Services.AddDocumentSeeder("lookups", version: 1, async (store, ct) =>{ await store.Upsert(new Country { Id = "CA", Name = "Canada" }, cancellationToken: ct);});IDocumentMaintenance.ClearAll() — whole-store reset across providers — generalises the SQLite-only ClearAllAsync to every backend. Probe with store is IDocumentMaintenance and call ClearAll() to wipe every document type (plus temporal-history, spatial, and vector sidecars) — ideal for test/dev resets. It is a whole-store wipe, not tenant- or type-scoped (use Clear<T>() for a single type); on a shared-table multi-tenant store it clears all tenants. Implemented on the relational DocumentStore (SQLite, SQL Server, PostgreSQL, MySQL, DuckDB, Oracle), MongoDB, and Cosmos; SQLite’s existing ClearAllAsync now delegates to it. Verified against the SQLite and DuckDB suites — other backends follow the same tier-by-provider rollout as temporal.
8.0 - June 19, 2026
Section titled “8.0 - June 19, 2026”Interceptors can be registered from DI — in addition to AddInterceptor / AddBulkInterceptor on the options, AddDocumentStore now resolves every IDocumentInterceptor and IDocumentBulkInterceptor from the service container, so interceptors get constructor-injected dependencies (e.g. a logger or an outbox). Options-registered interceptors run first, then DI-registered ones in registration order. Resolved once from the store’s provider — register interceptors as singletons (use IServiceScopeFactory inside the hook if you need scoped services).
Enum fields in WhereIn / comparisons on PostgreSQL & DuckDB — the strict-typed providers extract JSON values with an explicit cast, but enum-typed fields fell through to a raw text extract, so WhereIn(x => x.Status, [...]) (and == on an enum field) failed with operator does not exist: text = integer. Enum fields are now cast to their underlying numeric type, matching how enums are stored. Loose-typed providers (SQLite, etc.) were unaffected.
RunInTransaction removed — use UnitOfWork + SaveChanges — grouping writes into one transaction is now done through a unit of work created from the store, the single public way to open a transaction. CreateUnitOfWork() is a first-class method on IDocumentStore (the old DocumentStoreExtensions.CreateUnitOfWork extension is gone), and UnitOfWork.Commit is renamed to SaveChanges (a [Obsolete] Commit alias forwards for now). Contiguous same-type inserts in a unit are coalesced into the fast batch-insert path, so grouping inserts is as fast as BatchInsert. Migrate store.RunInTransaction(tx => { await tx.Insert(a); await tx.Update(b); }) to:
var uow = store.CreateUnitOfWork();uow.Add(a).Update(b);await uow.SaveChanges();A unit is a write buffer, not a tracking context — reads don’t see uncommitted buffered writes. For read-modify-write atomicity, use ETag/CAS (IfMatch) + retry. Applies to every provider.
Write interceptors — register IDocumentInterceptor (per-document) and IDocumentBulkInterceptor (set-based) to observe and mutate writes. The after-hook runs inside the transaction, after the write succeeds and before commit, with the generated id/version populated — enabling transactional outbox patterns. Per-document interceptors fire for Insert/BatchInsert (per item)/Update/Upsert/Remove; bulk interceptors fire once for ExecuteUpdate/ExecuteDelete/Clear. BeforeWrite can mutate the document or throw to abort; temporal-driven writes (Restore) are flagged Source = Temporal. Register via OnBeforeWrite<T> / OnAfterWrite<T> lambdas or AddInterceptor / AddBulkInterceptor. Supported across every provider.
opts.AddInterceptor(new AuditInterceptor());opts.OnBeforeWrite<Order>((ctx, ct) => { /* validate / mutate ctx.Document */ return Task.CompletedTask; });7.2.1 - June 18, 2026
Section titled “7.2.1 - June 18, 2026”Bundled sqlite-vec for iOS, Android & desktop — Shiny.DocumentDb.Sqlite.VectorSupport — a new companion package ships the sqlite-vec native binaries (iOS static xcframework, Android .so per ABI, and macOS/Linux/Windows/Mac Catalyst loadables) and a one-call registration helper, so vector search works on every platform with no manual native setup. SqliteVec.RegisterAutoExtension() registers vec0 as a SQLite auto-extension — the only mechanism that works on iOS, where loose extensions can’t be dlopened — and SqliteVec.CreateProvider(connectionString) returns a ready provider with VectorExtensionPreloaded set. Registration is engine-aware, so it works with SQLCipher too (vec0 is registered against the e_sqlcipher engine — set VectorExtensionPreloaded = true on your SqlCipherDatabaseProvider).
// one PackageReference + one call, then map vectors as usualopts.DatabaseProvider = SqliteVec.CreateProvider($"Data Source={dbPath}");SQLite vector search on iOS — EnableVectorExtension loads sqlite-vec via sqlite3_load_extension, which cannot work on iOS (Apple forbids dlopen of loose libraries, and the bundled e_sqlite3 disables runtime extension loading) and usually fails on Android too. Previously this surfaced as a cryptic load failure. A new SqliteDatabaseProvider.VectorExtensionPreloaded flag supports the only workable mobile path: statically link sqlite-vec and register it once via sqlite3_auto_extension(sqlite3_vec_init) at startup, then set VectorExtensionPreloaded = true to skip the runtime load entirely. SupportsVector returns true, and if both flags are set the preloaded path wins. The load-failure exception now includes platform-specific guidance on iOS/Android. Most apps should use the new Shiny.DocumentDb.Sqlite.VectorSupport package instead of wiring this by hand.
new SqliteDatabaseProvider(connectionString){ VectorExtensionPreloaded = true // vec0 statically linked + auto-registered};7.2 - June 14, 2026
Section titled “7.2 - June 14, 2026”Scalar functions, flag-enum & phonetic queries — Where predicates now translate a library of scalar functions across every provider: string functions (ToLower/ToUpper, Length, Trim/TrimStart/TrimEnd, Substring, Replace, IndexOf, string.IsNullOrEmpty, string concatenation), Math.* (Abs, Round, Ceiling, Floor, Sqrt, Pow, Sign), date-part access (Year/Month/Day/…), and flag-enum tests — permissions.HasFlag(Permissions.Write) and the (x & flag) == flag idiom. The relational providers emit native SQL (BITAND on Oracle); MongoDB uses $expr aggregation ($toLower/$strLenCP/$substrCP/… and $bitsAllSet for flags); CosmosDB uses native NoSQL functions; LiteDB/IndexedDB evaluate in-memory. Phonetic search arrives via DocumentFunctions.Soundex(...), translated to native SOUNDEX() (SQL Server / MySQL / Oracle) or a registered connection UDF (SQLite); the same canonical implementation runs in-memory. You can register your own translations with options.MapFunctionTranslation(...). Internally the Where translator was refactored onto a shared, per-provider query IR. Soundex is also supported on PostgreSQL via the fuzzystrmatch extension, and on DuckDB/CosmosDB/MongoDB via a precomputed stored field (see Querying › Phonetic search).
var smiths = await store.Query<Account>() .Where(a => DocumentFunctions.Soundex(a.Name) == DocumentFunctions.Soundex("Smith")) .ToList();
var writers = await store.Query<Account>() .Where(a => a.Permissions.HasFlag(Permissions.Write)) .ToList();CosmosDB server-side filtering now works — CosmosDB stored each document’s body as an escaped JSON string in the data property, so c.data.field paths never resolved and every Where predicate (and query filter) silently matched zero rows server-side. Documents are now stored as nested JSON objects, so filtering, scalar functions, and flag-enum queries run on the server. A second bug that corrupted multi-parameter predicates (Substring, WhereIn) was fixed at the same time. Migration: documents written by earlier versions are still readable, but must be re-saved (e.g. Update/Upsert) to be matched by server-side filters — old rows keep the legacy string data until rewritten.
Scalar functions in the string Where & Project DSL — the runtime string grammar (for REST ?filter=/?fields=, saved views, admin search) now exposes the same scalar functions as the LINQ API: lower/upper, length, trim, substring, replace, indexof, abs/round/ceiling/floor/sqrt/sign, year/month/day/…, soundex, and the predicate forms isnullorempty/hasflag (alongside the existing contains/startsWith/endsWith). Functions nest and work on either side of a comparison. Projections add an as alias form for functions. Same AOT-safe translation as the compiled API.
store.Query<User>().Where("year(created) = 2026 and lower(name) = 'alice'");store.Query<User>().Project("name, lower(email) as email, year(created) as yr");Project(string) is now also supported on CosmosDB, MongoDB, LiteDB, and IndexedDB (previously SQL-only) — they project client-side via the same compile-free path that runs their in-memory predicates, so fields and scalar functions work everywhere.
Query surface is fully NativeAOT-safe — the Where translator runs on a shared expression IR (no Expression.Compile()), and the in-memory providers (LiteDB/IndexedDB) plus client-side filters now use a compile-free tree-walking interpreter instead of Expression.Compile(), removing the last RequiresDynamicCode (IL3050) holes from the query path.
Set-membership queries — WhereIn / WhereNotIn — filter to documents whose property is (or isn’t) one of an in-memory collection of values. The collection is passed as a single value and lowered to each store’s native construct (relational IN (…), Cosmos IN, MongoDB $in, LiteDB/IndexedDB in-memory) rather than expanded into the query text, so one call behaves identically across every provider. null handling is explicit via a NullHandling argument (Ignore default / Match / Raw), an empty set is well-defined (WhereIn matches nothing, WhereNotIn everything), and a string property-name overload mirrors the string OrderBy/Where helpers.
var statuses = new[] { "Open", "Pending", "Review" };var open = await store.Query<Order>() .WhereIn(o => o.Status, statuses) .ToList();The string filter’s field in (…) form now lowers through the same path, so Where("Status in ('Open','Pending')") and WhereIn(o => o.Status, …) produce identical native queries. See Querying › Set membership
Guid and enum field comparisons bind correctly across providers — predicates over a Guid property (e.g. Where(x => x.Ref == id)) or an enum property now match reliably on every relational provider. Previously a boxed Guid or enum parameter was bound in a provider-dependent shape that didn’t match the value’s JSON representation (notably Guid on SQLite and enum on DuckDB silently returned no rows). Guids are now bound as their System.Text.Json string form and enums as their underlying numeric value, so the comparison is identical to the corresponding string/number field. Applies to Where, WhereIn/WhereNotIn, and the string filter.
Optional JsonTypeInfo on the string query helpers — Where(string), OrderBy(string) / OrderByDescending(string) (incl. the direction overload), and Project(string) no longer require a JsonTypeInfo argument. When omitted, the query reuses the metadata it already resolved at creation (from Query(ctx.User) or the registered JsonSerializerContext), so the common case loses the redundant re-passing:
var results = await store.Query(ctx.User) .Where("Age >= 30") .OrderBy("Name", "desc") .ToList();Pass one explicitly to override; reflection-only queries (no resolvable context) still require it.
Runtime string filters — Where(string, JsonTypeInfo<T>) — filter with a human-friendly expression string supplied at runtime (a REST ?filter=, a saved view, an admin search box) instead of a compiled lambda. Supports and/or/not with parentheses, comparisons (==/=, !=/<>, >, >=, <, <=), field is [not] null, field in (a, b, c), and contains/startsWith/endsWith(field, 'x'). Field names match the string-OrderBy rules (case-insensitive CLR or JSON name, dotted paths) and literals are coerced to each field’s CLR type.
var open = await store.Query<User>() .Where("Age >= 30 and Status == 'open'", ctx.User) .ToList();It parses to the same expression tree a compiled predicate produces and runs through the existing translator, so it never calls Compile() and resolves fields through JsonTypeInfo — fully AOT/trim-safe. See Querying › String-based Where
Interpolated Where($"…") — parameterized filter values — supply runtime values to a string filter as an interpolated string and each {value} hole is captured as a typed argument and bound as a parameter rather than formatted into the filter. You no longer quote string values or escape embedded quotes, and a hostile value can’t tamper with the filter (the Dapper / InterpolatedSql pattern). Holes are valid anywhere a literal would appear — comparison right-hand side, in (...) list, or contains/startsWith/endsWith argument — but never as a field name; values are coerced to the field’s CLR type and a null becomes an is null check.
var status = request.Query["status"];var open = await store.Query<User>() .Where($"Age >= {minAge} and Status == {status}", ctx.User) .ToList();An interpolated literal binds to this overload in preference to the raw Where(string) overload, so both coexist — pass a plain string (the raw ?filter= text) for the parsed form, an interpolated $"..." to capture values. Shares the same AOT-safe expression-tree path as Where(string). See Querying › Interpolated filters
Runtime field projection — Project(fields, JsonTypeInfo<T>) — project a runtime-chosen field list into IDocumentQuery<JsonObject> with no result DTO, the natural fit for REST sparse fieldsets (?fields=name,email). Rows come back as reflection-free JsonObject; pagination, Count, Any, and streaming all work on the projected query.
IReadOnlyList<JsonObject> rows = await store.Query<User>() .Where("Age >= 30", ctx.User) .Project("Name, Email", ctx.User) .ToList();Emits a json_object('name', json_extract(Data,'$.name'), …) projection; each output key is the leaf JSON name (duplicate leaves throw). Supported on the SQL providers. See Projections › Runtime field projection
Directional string sort — OrderBy(name, direction, jsonTypeInfo) — supply the sort direction as a runtime string alongside the column, e.g. for ?sort=name&dir=desc. Accepts asc/ascending/desc/descending (case-insensitive); an empty/null/whitespace direction defaults to ascending, and an unrecognized value throws. Delegates to the existing string OrderBy/OrderByDescending overloads, so it shares their AOT-safe resolution.
var results = await store.Query<User>() .OrderBy(request.Query["sort"], request.Query["dir"], ctx.User) .ToList();7.1 - June 13, 2026
Section titled “7.1 - June 13, 2026”Orleans grain storage (Shiny.DocumentDb.Orleans) — a Microsoft Orleans IGrainStorage (+ PubSubStore) provider implemented entirely against the backend-agnostic IDocumentStore, so one implementation runs on every DocumentDb backend. The Orleans contract maps cleanly onto the store: the document key is "{stateName}|{grainId}", the ETag is a GrainStateRecord.Version mapped via MapVersionProperty, and a ConcurrencyException surfaces as Orleans’ InconsistentStateException. Grain state is persisted as structured, nested JSON — so you can query grain state directly without activating the grains (json_extract(Data, '$.state.…') against the grain-state table — reporting/dashboards/admin over the persisted read model, which Orleans’ point-key storage contract can’t do) — and the envelope can opt into MapTemporal<GrainStateRecord> for a free audit trail of every state mutation, neither of which Orleans’ built-in providers offer.
// Relational backends — built-in pathsiloBuilder.AddDocumentDbGrainStorage("Default", o => o.DatabaseProvider = new PostgreSqlDatabaseProvider(connectionString));First-class companion packages Shiny.DocumentDb.Orleans.MongoDb and Shiny.DocumentDb.Orleans.CosmosDb wire the store, grain-state mapping, and version property for you (siloBuilder.AddMongoDbGrainStorage(...) / AddCosmosDbGrainStorage(...)); a StoreFactory escape hatch covers any other backend (LiteDB, IndexedDB, …). Compatibility tiers: Recommended PostgreSQL / SQL Server / MySQL / Oracle (atomic UPDATE … WHERE CAS, honored even during failover duplicate-activation windows); Supported MongoDB (atomic version-predicate filter); Limited/dev SQLite, LiteDB, IndexedDB, DuckDB; Use with care Cosmos DB (CAS is correct, but it partitions by typeName — weigh the 20 GB logical-partition limit for large single-type grain populations). Covered by integration tests on PostgreSQL + MongoDB, including a stale-write CAS conflict. There is no first-party Orleans MongoDB provider, so this fills a real gap. See Orleans Provider
Orleans system stores — reminders, clustering & grain directory — the Orleans persistence stack on Shiny.DocumentDb.Orleans now goes beyond grain storage. Each is registered with its own silo-builder extension and shares the same OrleansStoreOptions shape (relational DatabaseProvider built-in path, or a StoreFactory escape hatch for MongoDB / Cosmos / others); per-row optimistic concurrency rides on the same version-property CAS.
siloBuilder .AddDocumentDbReminders(o => o.DatabaseProvider = new PostgreSqlDatabaseProvider(cs)) .AddDocumentDbClustering(o => o.DatabaseProvider = new PostgreSqlDatabaseProvider(cs)) .AddDocumentDbGrainDirectory("Default", o => o.DatabaseProvider = new PostgreSqlDatabaseProvider(cs));- Reminders (
IReminderTable) —AddDocumentDbReminders(...)(also calls Orleans’AddReminders()), default tableorleans_reminders. Hash-ring range reads via a fluent query on the storedGrainHash; per-row version CAS. No multi-document transaction required, so it works on any backend. - Cluster membership (
IMembershipTable) —AddDocumentDbClustering(...), default tableorleans_membership. Per-silo rows and a global table-version row are updated together insideRunInTransaction, each CAS-gated. Requires multi-document transactions → relational or MongoDB replica set; Cosmos is not supported (single-partition batches only). - Grain directory (
IGrainDirectory) —AddDocumentDbGrainDirectory("Default", ...), default tableorleans_graindirectory. Per-row version CAS for register/unregister races; no transaction required.
Covered by PostgreSQL integration tests (ReminderTableTests, MembershipTableTests, GrainDirectoryTests). See Orleans Provider › System stores
Source-generated (reflection-free) Orleans serialization — the Orleans provider’s internal envelope/document types (grain-state record, reminders, membership, grain directory) are now always serialized through a source-generated JsonSerializerContext, so the store handles them without reflection. Grain state T becomes source-generated too when you assign a JsonSerializerContext as o.JsonSerializerOptions.TypeInfoResolver; the new UseReflectionFallback flag (on grain-storage and all system-store options) throws a clear exception for an unregistered state type when set to false instead of falling back to reflection. Defaults (UseReflectionFallback = true, no context) preserve the prior behavior, so it’s purely opt-in. The AOT/trim analyzers are enabled on the package and the JSON-null tombstone no longer round-trips through the serializer. (The silo host itself remains a non-AOT target — Microsoft.Orleans.Runtime is reflection-heavy.) See Orleans Provider › Source-generated serialization
Atomic optimistic-concurrency CAS on MongoDB and Cosmos DB — the version-checked Update/Upsert paths previously read the stored version, compared it in memory, then wrote — a non-atomic read-then-write that could lose a concurrent writer’s update in the window between read and write (the failover edge case that bites Orleans grain storage). Both providers now perform a server-side atomic compare-and-swap: MongoDB folds the expected version into the UpdateOne filter (MatchedCount == 0 → ConcurrencyException), and Cosmos DB uses a native IfMatchEtag precondition on the replace (HTTP 412 → ConcurrencyException). Scoped to version-mapped types only; non-versioned writes keep last-write-wins, matching the relational providers (which were already atomic via UPDATE … WHERE version = @expected).
7.0 - June 12, 2026
Section titled “7.0 - June 12, 2026”Temporal support (system-time history) — opt a document type into append-only versioning with options.MapTemporal<T>(o => { ... }). Every Insert, Update, Upsert, Remove, SetProperty, RemoveProperty, and BatchInsert (including writes inside RunInTransaction) records a versioned snapshot to a per-type history sidecar, so a document’s state can be read back as of any point in time.
options.MapTemporal<Order>(o =>{ o.Retention = TimeSpan.FromDays(90); // prune expired versions older than this o.MaxVersions = 50; // …or cap versions per document o.CaptureActor = () => currentUser.Id; // optional "who" recorded per version});History query methods on the ITemporalDocumentStore capability interface (ITemporalDocumentStore : IDocumentStore) — a sibling of IObservableDocumentStore / IChangeFeedDocumentStore, not on the base IDocumentStore, since history is an optional capability rather than universal CRUD (same reasoning as the Backup/ClearAllAsync precedent). Per-document History<T>(id), AsOf<T>(id, when), Restore<T>(id, version), and GetDiffBetween<T>(id, from, to) (RFC 6902 patch between two versions — the temporal analogue of GetDiff); plus fleet-wide AsOfAll<T>(when) (point-in-time snapshot of every live document), ChangesByActor<T>(actor) (per-user audit trail), and ChangesBetween<T>(from, to) (audit log over a time window). Reads return DocumentVersion<T> (Id, Version, ValidFrom, ValidTo, Operation, Actor, Document); Remove records a null-body tombstone so AsOf returns null after a deletion.
Implemented on every provider — the relational stores (SQLite, SQLCipher, PostgreSQL, SQL Server, MySQL, Oracle, DuckDB) plus the document stores (LiteDB, MongoDB, CosmosDB, IndexedDB). Each persists versions to its own sidecar: a {table}_history table (relational, with a (Id, TypeName, Version) PK and (TypeName, ValidFrom, ValidTo) / (TypeName, Actor) secondary indexes), a {collection}_history collection (LiteDB, MongoDB), a {container}_history container partitioned by /typeName (CosmosDB), or a {store}_history object store (IndexedDB). The post-image read-back for merge/property writes and all history storage are incurred only for temporal-mapped types; non-temporal types are untouched. Retention (Retention by age, MaxVersions by count) is pruned on every write; the current version is never pruned. Clear<T> does not record per-document history. IndexedDB: bump options.Version when adding MapTemporal to an already-deployed database so the schema upgrade creates the history object stores. See Temporal Support
Telemetry & diagnostics (Shiny.DocumentDb.Diagnostics) — OpenTelemetry-native metrics and distributed tracing for any provider via a drop-in decorator. Register a store, then services.AddDocumentStoreInstrumentation(), and subscribe with .AddMeter("Shiny.DocumentDb") / .AddSource("Shiny.DocumentDb").
services.AddDocumentStore(o => o.DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db"));services.AddDocumentStoreInstrumentation();Built on System.Diagnostics.Metrics.Meter (created via IMeterFactory) and ActivitySource, following the OpenTelemetry database client semantic conventions: a db.client.operation.duration histogram (plus an operations counter and a returned-rows histogram), tagged db.system.name / db.operation.name / db.collection.name / outcome / error.type, and a {system}.{operation} client span per call with error status + exception capture. db.system.name is derived from the wrapped store, so it works across all 11 providers with no per-provider config. Coverage spans CRUD, the fluent-query terminals (ToList/Count/Any/ExecuteDelete/ExecuteUpdate/aggregates), the temporal ITemporalDocumentStore operations, and RunInTransaction (inner operations become child spans of the transaction span). Zero-cost when nothing is listening; never records document bodies, ids, or parameter values. NotifyOnChange/SubscribeChanges pass through untraced. See Telemetry & Diagnostics
6.2.0 - June 10, 2026
Section titled “6.2.0 - June 10, 2026”UseGuidV7Ids()) — opt into time-ordered (version 7) GUID generation for Guid document Ids instead of the default random v4, via Guid.CreateVersion7(). No new dependency (BCL), and the storage format is unchanged, so it is a drop-in for existing Guid-keyed data — only newly generated Ids differ. Shorthand for MapIdType(new GuidV7IdConverter()). See CRUD › Sortable Guid IdsCustom document Id types (MapIdType) — document Ids are no longer limited to Guid, int, long, and string. Register a converter on the store options to use any CLR type — a Ulid, or a strongly-typed wrapper such as record struct OrderId(Guid Value):
options.MapIdType( toString: (OrderId id) => id.Value.ToString("N"), parse: s => new OrderId(Guid.ParseExact(s, "N")), isDefault: id => id.Value == Guid.Empty, generate: OrderId.New); // optional auto-generation on InsertA DocumentIdConverter<TId> base class is available for reusable/testable converters. The converter defines four things: ToStorageString, FromStorageString, IsDefault (when to auto-generate on Insert), and an optional TryGenerate. The Id is still stored as a string in every provider’s envelope (SQL Id column, Mongo _id/id, Cosmos id), so there is no schema or on-disk change. Insert, Get, Update, Remove, and Upsert all accept the strongly-typed Id. Purely additive — the built-in Guid/int/long/string types behave exactly as before with no registration. Available on every provider’s options (DocumentStoreOptions, CosmosDbDocumentStoreOptions, MongoDbDocumentStoreOptions, LiteDbDocumentStoreOptions, IndexedDbDocumentStoreOptions). Note: LINQ predicates on the Id property (Where(x => x.Id == value)) compare against the JSON document, so the type needs a matching System.Text.Json converter for the serialized form to line up. See CRUD › Custom Id types
.Count / array .Length property form in predicates and projections — Where(o => o.Lines.Count == 0), Where(o => o.Tags.Count > 1), and projections like Select(o => new R { N = o.Lines.Count }) now translate to the same native array-length function as the .Count() method (json_array_length, jsonb_array_length, JSON_LENGTH, OPENJSON … COUNT, JSON_VALUE … .size(), ARRAY_LENGTH, and MongoDB $size) across every provider. Previously the property form was silently mistranslated to a non-existent JSON path (json_extract(Data, '$.lines.count')) that returned NULL and matched nothing — no exception, just wrong results — in both Where and Select. As part of the fix, size-like accesses that are not JSON array lengths (string.Length, dictionary .Count) now throw NotSupportedException instead of generating the same dead path — use .Count() / .Any() for collection length. A real document property literally named Count or Length still resolves normally. See Querying › Collection Count and ProjectionsShiny.DocumentDb.Oracle package — Oracle Database provider built on Oracle.ManagedDataAccess.Core (ODP.NET). Requires Oracle 23ai or later. Documents are stored as IS JSON-checked CLOB columns; Upsert runs server-side with true RFC 7396 deep merge via JSON_MERGEPATCH; SetProperty/RemoveProperty route through auto-provisioned PL/SQL helper functions (Oracle’s JSON_TRANSFORM only accepts literal paths); JSON property indexes are function-based on JSON_VALUE. A dialect adapter wraps every connection so the core’s @name placeholder conventions, name-based binding, and CLOB-sized strings all just work — raw SQL keeps the same @name syntax as every other provider. Full feature parity with MySQL: LINQ translation, projections, aggregates, batch insert, pagination, multi-tenancy, optimistic concurrency, query filters, and in-process change monitoring — verified by the full provider integration suite running against gvenzl/oracle-free via Testcontainers. Spatial and native change feeds are not supported. See OracleMapVectorProperty<T> / NearestVectors on top of Oracle 23ai’s native AI Vector Search. Embeddings are stored in a per-type sidecar table with a VECTOR(n, FLOAT32) column; VECTOR_DISTANCE(...) powers ranking (Cosine, Euclidean, DotProduct — Hamming throws) and TO_VECTOR binds the query vector. VectorIndexKind.Hnsw (ORGANIZATION INMEMORY NEIGHBOR GRAPH) and Ivf (ORGANIZATION NEIGHBOR PARTITIONS) emit a CREATE VECTOR INDEX; index creation is wrapped so databases without a configured vector_memory_size pool silently fall back to an exact sequential scan (which VECTOR_DISTANCE still serves correctly), and FETCH APPROX is used only when an index kind is requested. Where(...) predicates pre-filter via the JOIN back to the documents table. This brings the relational vector-capable provider set to PostgreSQL, SQL Server 2025, and Oracle 23ai. See Vector and Oracle6.1.0 - June 2, 2026
Section titled “6.1.0 - June 2, 2026”PageResult(page, pageSize, zeroBased?) extension on IDocumentQuery<T> — runs the query and returns PagedResults<T> { Records, TotalCount, Page, PageSize } in one call. TotalCount reflects the current Where filters (and global query filters) — pagination state is ignored when counting. 1-based by default to match common UI/REST conventions; pass zeroBased: true for 0-based indexing. Overrides any prior .Paginate(...) call on the query. See PaginationOrderBy / OrderByDescending extensions on IDocumentQuery<T> — sort by a property identified at runtime by name (query.OrderBy("Name", ctx.User)). Matches case-insensitively against either the CLR property name or the JSON property name (after naming policy). Supports dotted paths for nested properties ("ShippingAddress.City"). Fully AOT-safe: resolution walks JsonTypeInfo.Properties (source-generated) and synthesizes an Expression.Property(parameter, PropertyInfo) tree — no Type.GetProperty(string) reflection on T, no Expression.Compile(). Intended for dynamic UIs where the sort column is user-selected at runtime. See Ordering6.0 - June 1, 2026
Section titled “6.0 - June 1, 2026”CreateIndexAsync<T>(JsonTypeInfo<T>, params IEnumerable<Expression<Func<T, object>>>) (and matching DropIndexAsync) build a single B-tree over multiple JSON paths. SQLite, SQLCipher, PostgreSQL, MySQL, and DuckDB emit one composite index with one expression per path; SQL Server adds a PERSISTED computed column per path (cc_{indexName}_0, cc_{indexName}_1, …) and indexes them all. Existing single-path overloads keep the legacy index/column names so nothing on disk has to change. Drop discovers the index’s backing computed columns via sys.index_columns and removes them after the index, so single- and multi-column drops use the same code path. See Indexes › CompositeMapVectorProperty<T>(d => d.Embedding, dimensions: 1536, metric: VectorDistance.Cosine, indexKind: VectorIndexKind.Hnsw) and query with store.Query<T>().Where(...).NearestVectors(queryEmbedding, k: 10). Returns VectorResult<T> ({ Document, Score }) ordered nearest first. Provider-native indexes: pgvector (PostgreSQL HNSW/IVF + all four metrics including Hamming), native VECTOR(n) + VECTOR_DISTANCE (SQL Server 2025, DiskANN), embedding policy + VectorDistance() (CosmosDB DiskANN/QuantizedFlat/Flat), $vectorSearch aggregation (MongoDB Atlas HNSW), vss extension (DuckDB HNSW), sqlite-vec virtual table (SQLite flat scan with post-filter candidate multiplier). Pre-filter via Where(...) on every provider that supports it; SQLite post-filters with a configurable multiplier. Cosine score is always surfaced as distance in [0, 2] regardless of provider convention. LiteDB, IndexedDB, and MySQL throw NotSupportedException. See VectorAutoEmbedOnInsert<T> (Shiny.DocumentDb.Extensions.AI) — plug Microsoft.Extensions.AI.IEmbeddingGenerator<string, Embedding<float>> into the new DocumentStoreOptions.OnBeforeInsert<T> pipeline so a text property is automatically embedded into a ReadOnlyMemory<float> field on Insert, BatchInsert, and Upsert. Skips when the source is null/empty or when the target already holds a non-default vector — explicit writes win over the generator. See Vector › Auto-embedVectorIndexOptions — strongly-typed knobs for HNSW (M, EfConstruction, EfSearch) and IVF (Lists) plus a ProviderHints dictionary for the long tail (sqlite.postFilterMultiplier, atlas.indexName, atlas.numCandidates)OnBeforeInsert<T> hook on DocumentStoreOptions — register an async handler that runs on every document before serialization on Insert/BatchInsert/Upsert. Handlers run in registration order. Used by AutoEmbedOnInsert<T> but available for any “compute derived fields” scenarioSupportsVector property on IDocumentStore and IDatabaseProvider — check vector-search availability at runtime. IDocumentStore.NearestVectors<T>(query, k, filter?) is on the interface with a default-throwing implementation, so existing providers compile without changesDocumentStore now opens a connection per operation on PostgreSQL, MySQL, and SQL Server, relying on the ADO.NET driver’s built-in connection pool. A single store instance can serve concurrent callers without the operation-serializing semaphore that previous releases used. SQLite and DuckDB (embedded engines) keep the long-lived shared connection + semaphore model — opt in by overriding IDatabaseProvider.RequiresSingleConnection => true. Table init is now backed by a ConcurrentDictionary<string, Lazy<Task>> so first-touch DDL runs exactly once per table even under concurrent first calls. RunInTransaction pins one connection for the user callback so nested ops share the transaction@data in a CAST(...) expression (Postgres’ CAST(@data AS JSONB), DuckDB’s CAST(@data AS JSON)) skipped the value-list rewrite, so INSERT ended up with 6 columns and 5 values. The substitution now anchors on (@id, @typeName, so it survives provider variationsData #>> '{Version}' = @expectedVersion, which Postgres rejects with 42883: operator does not exist: text = integer. Switched to JsonExtractTyped(..., typeof(int)) so providers emit the proper ::BIGINT (or equivalent) cast on the extracted valueShiny.DocumentDb.MongoDb package — MongoDB provider for Shiny.DocumentDb. Implements the full IDocumentStore API over MongoDB.Driver, storing each document as a typed BSON envelope (_id = "{TypeName}:{Id}", id, typeName, data, createdAt, updatedAt) inside a configurable collection. Includes MapTypeToCollection<T> for collection-per-type isolation, MapVersionProperty<T> for optimistic concurrency, and a sharable MongoClient for pooled clients. See MongoDBShiny.DocumentDb.DuckDb package — embedded analytical store backed by DuckDB. Plugs into the standard IDatabaseProvider pipeline like SQLite/Postgres/MySQL/SQL Server, with native JSON column storage and server-side RFC 7396 Upsert via DuckDB’s json_merge_patch. The json extension is auto-loaded on every connection. See DuckDBIObservableDocumentStore) — consume an IAsyncEnumerable<DocumentChange<T>> of insert/update/remove/clear events with await foreach (var c in store.NotifyOnChange<User>(ct)). Channel-based fan-out — each subscriber gets its own bounded reader and unsubscribes automatically when the iterator exits or the token cancels. Changes inside RunInTransaction are buffered and emitted on commit; rollbacks discard them. Supported on DocumentStore (SQLite, SQLCipher, MySQL, SQL Server, PostgreSQL) and LiteDbDocumentStore. See Change Monitoring.NotifyOnChange(ct) which filters the change stream by the query’s Where predicates: store.Query<Order>().Where(o => o.Status == "Pending").NotifyOnChange(ct). OrderBy, Paginate, and GroupBy are ignored (they affect result shape, not membership). Throws after Select(...). Property-level events (SetProperty/RemoveProperty/Remove/Clear, where Document == null) are passed through unconditionally so consumers can re-queryWhenDocumentChanged<T>(id) extension — filters the in-process change stream to events for a single document Id (plus Cleared, which affects every document of the type)IChangeFeedDocumentStore) — SubscribeChanges<T> observes the underlying database itself, including writes from other processes / connections / store instances. PostgreSQL uses LISTEN/NOTIFY with row-level triggers (true push), SQL Server uses Change Tracking with optional SqlDependency query notifications (configurable via SqlServerChangeFeedOptions), and Cosmos DB uses the native Change Feed API with an auto-provisioned lease container. Provisioning is automatic and idempotent. Throws NotSupportedException on SQLite, LiteDB, IndexedDB, MySQL, and DuckDBDocumentChange<T> envelope — ChangeType (Inserted / Updated / Removed / Cleared), Id, and Document (populated for Inserted and full-document Updated; null for Removed / Cleared / property-level updates)MapIdProperty<T>(...) — standalone Id-property override that no longer requires MapTypeToTable. Use it when the document Id is not literally named Id (e.g. BlogPost.Slug) but you still want the type stored in the default shared table. Expression and AOT-safe string overloadsAddQueryFilter<T>) — register a predicate that’s automatically AND-applied to every query of T, mirroring Entity Framework Core’s HasQueryFilter. Supports unnamed and named filters (AddQueryFilter<T>("name", ...)) with per-query opt-out via IgnoreQueryFilters() or IgnoreQueryFilters("name"). Filters apply to Query<T>() and every terminal, single-document operations (Get/Update/Remove/SetProperty/RemoveProperty/Clear), bulk operations (ExecuteUpdate/ExecuteDelete), and per-query change monitoring. Insert/BatchInsert/Upsert and raw SQL are intentionally unfiltered (matches EF Core). Captured variables are re-read on every translation, so per-request values (multi-tenancy, soft-delete, row-level scopes) work without rebuilding the store. Available on DocumentStoreOptions, LiteDbDocumentStoreOptions, CosmosDbDocumentStoreOptions, MongoDbDocumentStoreOptions, and IndexedDbDocumentStoreOptions. See Global Query FiltersUpsert performs RFC 7396 deep merge in C# with recursive null stripping, matching CosmosDB / LiteDB / IndexedDB semanticsRunInTransaction uses a compensating model (track inserts, delete on failure) for single-node deployments. Matches the CosmosDB provider’s behaviour. Use a replica set + custom session for true ACID multi-document transactionsSetProperty and RemoveProperty are implemented via json_merge_patch — DuckDB has no json_set/json_remove, so the JSON path is folded into a synthetic merge-patch document server-side using list_reduce. RFC 7396 null = delete semantics are preserved on RemovePropertyQuery<T>(string) / QueryStream<T>(string) raw SQL parity with the other SQL providers (use json_extract_string(Data, '$.path'))5.2.2 - May 30, 2026
Section titled “5.2.2 - May 30, 2026”CreateIndexAsync emitted broken DDL — the jsonPath argument was ignored; every JSON-path index registration silently produced a useless index on the TypeName column. Indexes are now backed by a persisted computed column (cc_{indexName}) over JSON_VALUE(Data, '$.path'), with a filtered CREATE INDEX on that column. DropIndex now drops both the index and its backing computed column using the required DROP INDEX … ON [table] syntaxjsonb || jsonb concat operator (top-level only) and SQL Server used a flat OPENJSON … FULL OUTER JOIN, both of which clobbered nested objects. Neither database has a native RFC 7396 JSON_MERGE_PATCH. Both providers now perform a row-locked read-merge-write fallback in C# (using SELECT … FOR UPDATE on PG and WITH (UPDLOCK, HOLDLOCK) on SQL Server), keeping the documented RFC 7396 deep-merge semanticsnew Doc { Address = new Address { City = "X" } }), the unfilled street/state nulls reached the merge step and were interpreted as RFC 7396 deletions, silently wiping the stored values. Null-stripping is now recursive, so partial nested patches preserve unspecified fields on SQLite, MySQL, LiteDB, IndexedDB, Cosmos DB, PostgreSQL, and SQL ServerDropIndexAsync emitted DROP INDEX {name};, which is invalid in MySQL. Now emits proper drop index on table5.2.1 - May 29, 2026
Section titled “5.2.1 - May 29, 2026”CreateUnitOfWork() extension method on IDocumentStore returns a UnitOfWork that buffers Add/Update/Remove operations and applies them atomically inside a single transaction on Commit(). The queue is auto-cleared on successful commit; on failure the transaction is rolled back and the queue is preserved for inspection or retry. Works across every provider via RunInTransaction. See Unit of Work5.2 - May 27, 2026
Section titled “5.2 - May 27, 2026”[JSImport] from System.Runtime.InteropServices.JavaScript instead of IJSRuntime.InvokeAsync. The library no longer requires JsonSerializerIsReflectionEnabledByDefault=true to function — apps targeting AOT or trim-safe deployments can use the IndexedDB provider without re-enabling reflectionJsonSerializerContext (camelCase) for DocumentRecord wire-format serialization — the library no longer depends on the consuming app’s JsonSerializerOptions for envelope types. Existing IndexedDB databases remain readable; the wire format is unchangedIJSRuntime.InvokeAsync<IJSObjectReference>("import", ...) to JSHost.ImportAsync(...). No app-side changes requiredIndexedDbDocumentStore no longer throws JsonSerializerIsReflectionDisabled when the host app has reflection disabled. In 5.1 and earlier, the library’s reliance on Blazor’s IJSRuntime Object[] arg envelope forced apps targeting AOT to either drop the IndexedDB provider or globally re-enable reflection5.1 - May 26, 2026
Section titled “5.1 - May 26, 2026”Count, Any, ToList, ToAsyncEnumerable, ExecuteDelete, ExecuteUpdate, Max, Min, Sum, Average, and projected Select queries) now initialize the type-specific table before executing. Previously, the query path always initialized the default TableName, so calling a query method against a type registered with MapTypeToTable<T>() before any insert had created its table raised no such table: <Name>Order, Group, User) no longer produces syntax error at table creation or insert time5.0 - May 6, 2026
Section titled “5.0 - May 6, 2026”Backup removed from IDocumentStore interface — now available only on concrete types: SqliteDocumentStore.Backup(), SqlCipherDocumentStore.Backup(), and LiteDbDocumentStore.Backup()AddSqliteDocumentStore, AddSqlCipherDocumentStore, AddSqlServerDocumentStore, AddMySqlDocumentStore, AddPostgreSqlDocumentStore, AddLiteDbDocumentStore, AddCosmosDbDocumentStore, AddIndexedDbDocumentStore). Use AddDocumentStore from Shiny.DocumentDb.Extensions.DependencyInjection insteadAddDocumentStore("name", opts => ...) registers stores as .NET keyed singletons. Inject with [FromKeyedServices("name")] or resolve dynamically via IDocumentStoreProvider.GetStore("name")Shiny.DocumentDb.Extensions.DependencyInjection: shared-table (single database with automatic TenantId column filtering) and tenant-per-database (separate database per tenant via lazy factory)ITenantResolver interface — implement to provide the current tenant ID. Used by both multi-tenancy strategies to auto-resolve tenant context per requestAddDocumentStore(configure, multiTenant: true) — shared-table multi-tenancy registration. Adds a dedicated TenantId column and index to the schema; all queries are automatically filtered by the current tenantAddMultiTenantDocumentStore(Func<string, DocumentStoreOptions>) — tenant-per-database registration. Each tenant gets a lazily-created separate database. IDocumentStore is registered as scoped and resolves to the correct tenant automaticallyTenantIdAccessor on DocumentStoreOptions — core pipeline hook for shared-table multi-tenancy. When set, all queries include a TenantId filter and all inserts include the tenant value. A dedicated column and index are created automaticallyShiny.DocumentDb.IndexedDb package — IndexedDB provider for Blazor WebAssembly with IndexedDbDocumentStore. Zero native dependencies, persists to the browser’s IndexedDB via JS interopSqliteDatabaseProvider now skips WAL pragma on OperatingSystem.IsBrowser(), spatial R*Tree is disabled in WASM, and Backup() is marked [UnsupportedOSPlatform("browser")]Shiny.DocumentDb.LiteDb package — LiteDB provider with LiteDbDocumentStoreShiny.DocumentDb.CosmosDb package — Azure Cosmos DB provider with CosmosDbDocumentStoreWithinRadius, WithinBoundingBox, and NearestNeighbors methods on IDocumentStore with default NotSupportedException for unsupported providersGeoPoint readonly record struct — represents a WGS84 coordinate, serializes as GeoJSON {"type":"Point","coordinates":[lng,lat]}GeoBoundingBox readonly record struct for area-based spatial queriesSpatialResult<T> wrapper — returns documents with computed DistanceMeters from the query center pointMapSpatialProperty<T> on DocumentStoreOptions — register which GeoPoint property to use for spatial indexing per document typeR*Tree virtual tables — automatic sidecar table creation and CRUD sync for spatial-indexed documentsST_DISTANCE and ST_WITHIN GeoJSON queries with automatic spatial index policySupportsSpatial property on IDocumentStore — check if the current provider supports spatial queries at runtimeSqliteDocumentStore.ClearAllAsync() — deletes all documents across all tables in the SQLite database, including spatial sidecar tablesMapVersionProperty<T>(x => x.RowVersion) on all provider options classes. Version is set to 1 on insert, checked and incremented on update/upsert. Throws ConcurrencyException on mismatch. Stored inside the JSON blob — zero schema changes requiredMapVersionProperty<T> overload — MapVersionProperty<T>(string propertyName, Func<T, int> getter, Action<T, int> setter) for trimming-safe deploymentsConcurrencyException — new exception type with TypeName, DocumentId, ExpectedVersion, and ActualVersion properties for diagnosing version conflicts4.0 - April 30, 2026
Section titled “4.0 - April 30, 2026”Shiny.DocumentDb.Extensions.AI package — exposes IDocumentStore operations as Microsoft.Extensions.AI tool functions for LLM agentsAddDocumentStoreAITools DI extension — opt-in registration of document types with per-type capability flags (ReadOnly, All, or individual Get/Query/Count/Aggregate/Insert/Update/Delete)All): get_by_id, query, count, aggregate, insert, update, deleteand/or/not combinators and leaf comparisons (eq, ne, gt, gte, lt, lte, contains, startsWith, in) — translated to LINQ expressions at runtimeDescription(), Property() description overrides, AllowProperties() / IgnoreProperties() for field visibility control, MaxPageSize() to cap query resultsJsonTypeInfo<T> from source-generated JSON contextscount, sum, min, max, avg functions with optional structured filtersDocumentStoreAITools wrapper class — resolve from DI and pass .Tools to IChatClient / ChatOptions.Tools3.2 - March 26, 2026
Section titled “3.2 - March 26, 2026”Shiny.DocumentDb.Sqlite.SqlCipher package — encrypted SQLite via SQLCipher with a separate native bundle, no changes to the existing Shiny.DocumentDb.Sqlite packageSqlCipherDatabaseProvider(filePath, password) — explicit file path and password parameters so users know exactly what is requiredSqlCipherDocumentStore convenience wrapper and AddSqlCipherDocumentStore DI extension for quick setupRekeyAsync extension method on IDocumentStore — change the encryption key of an existing SQLCipher database via PRAGMA rekey with SQL injection protectionDocumentStore.DatabaseProvider public property — exposes the underlying IDatabaseProvider for extension methods3.1 - March 24, 2026
Section titled “3.1 - March 24, 2026”SystemTextJsonPatch dependency — replaced with built-in AOT-compatible JsonPatchDocument<T> and JsonPatchOperation types that use JSON DOM manipulation instead of reflectionJsonPatchDocument<T>.ApplyTo() now returns a new T instead of mutating the target in place — var patched = patch.ApplyTo(original)JsonPatchOperation immutable type with static factory methods: Add, Replace, Remove, Copy, Move, TestJsonPatchDocument<T> with AOT-safe overload accepting JsonTypeInfo<T> — patch.ApplyTo(target, MyJsonContext.Default.MyType)BatchInsert<T> now uses multi-row INSERT statements chunked into batches of 500 rows, significantly reducing database round-trips — especially impactful for PostgreSQL3.0 - March 23, 2026
Section titled “3.0 - March 23, 2026”Shiny.SqliteDocumentDb to Shiny.DocumentDb with separate provider packages: Shiny.DocumentDb.Sqlite, Shiny.DocumentDb.SqlServer, Shiny.DocumentDb.MySql, Shiny.DocumentDb.PostgreSqlConnectionString removed from DocumentStoreOptions — replaced by required IDatabaseProvider DatabaseProvider. The connection string is now passed to each provider’s constructorShiny.SqliteDocumentDb.Extensions.DependencyInjection packageSqliteDocumentStore moved to Shiny.DocumentDb.Sqlite namespace. Base class is now DocumentStore in Shiny.DocumentDbShiny.DocumentDb.SqlServer with AddSqlServerDocumentStore DI extensionShiny.DocumentDb.MySql with AddMySqlDocumentStore DI extensionShiny.DocumentDb.PostgreSql with AddPostgreSqlDocumentStore DI extensionIDatabaseProvider interface — swap database backends without changing application code2.0 - March 22, 2026
Section titled “2.0 - March 22, 2026”Shiny.SqliteDocumentDb.Extensions.DependencyInjection package — the core library no longer depends on Microsoft.Extensions.DependencyInjection.Abstractionsnew SqliteDocumentStore("Data Source=mydata.db") for quick setup without optionsDocumentStoreOptions.TableName (defaults to "documents")MapTypeToTable<T>() gives a document type its own dedicated SQLite table with lazy creation on first useMapTypeToTable<T>() derives from the type name, MapTypeToTable<T>(string) uses an explicit nameInvalidOperationExceptionMapTypeToTable<T>("table", x => x.MyProperty) uses an alternate property as the document Id instead of the default IdMapTypeToTable overloads return DocumentStoreOptions for chainingGetDiff<T>(id, modified) — compares a modified object against the stored document and returns an RFC 6902 JsonPatchDocument<T> with deep nested-object diffing powered by SystemTextJsonPatchBatchInsert<T>(IEnumerable<T>) — inserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure1.0 - March 6, 2026
Section titled “1.0 - March 6, 2026”Id property on document types (Guid, int, long, or string) — stored in both the SQLite column and the JSON blob so query results always include itGuid → Guid.NewGuid(), int/long → MAX(CAST(Id AS INTEGER)) + 1 per TypeName. String Ids must be set explicitly — Insert throws for default string Idsobject (Guid, int, long, or string). Unsupported types throw ArgumentException