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

DocumentDB Releases

Feature

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);
Feature

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);
});
Feature

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.

Enhancement

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).

Fix

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.

Feature

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; });
Feature

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 usual
opts.DatabaseProvider = SqliteVec.CreateProvider($"Data Source={dbPath}");
Fix

SQLite vector search on iOSEnableVectorExtension 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
};
Feature

Scalar functions, flag-enum & phonetic queriesWhere 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 testspermissions.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();
Fix

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.

Enhancement

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.

Enhancement

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.

Feature

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

Fix

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.

Feature

Optional JsonTypeInfo on the string query helpersWhere(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.

Feature

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

Enhancement

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

Feature

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

Feature

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();

See Querying › String-based OrderBy

Feature

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 path
siloBuilder.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

Feature

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 table orleans_reminders. Hash-ring range reads via a fluent query on the stored GrainHash; per-row version CAS. No multi-document transaction required, so it works on any backend.
  • Cluster membership (IMembershipTable)AddDocumentDbClustering(...), default table orleans_membership. Per-silo rows and a global table-version row are updated together inside RunInTransaction, 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 table orleans_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

Feature

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

Fix

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 == 0ConcurrencyException), 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).

Feature

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

Feature

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

Feature
Sortable v7 Guid Ids (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 Ids
Feature

Custom 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 Insert

A 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

Feature
Collection .Count / array .Length property form in predicates and projectionsWhere(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 Projections
Feature
New Shiny.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 Oracle
Feature
Oracle vector / ANN search — the Oracle provider now implements MapVectorProperty<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 Oracle
Feature
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 Pagination
Feature
String-based OrderBy / 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 Ordering
Feature
Composite / multi-column JSON indexesCreateIndexAsync<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 › Composite
Feature
Vector / ANN search — register an embedding property with MapVectorProperty<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 Vector
Feature
AutoEmbedOnInsert<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-embed
Feature
VectorIndexOptions — 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)
Feature
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” scenario
Feature
SupportsVector 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 changes
Feature
Concurrent operations on server SQL providersDocumentStore 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
Fix
PostgreSQL & DuckDB multi-tenancy was silently broken — providers that wrap @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 variations
Fix
PostgreSQL optimistic concurrency was broken — the version check used Data #>> '{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 value
Feature
New Shiny.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 MongoDB
Feature
New Shiny.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 DuckDB
Feature
In-process change monitoring (IObservableDocumentStore) — 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
Feature
Per-query change monitoring — every fluent query exposes .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-query
Feature
WhenDocumentChanged<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)
Feature
Native change feeds (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 DuckDB
Feature
DocumentChange<T> envelope — ChangeType (Inserted / Updated / Removed / Cleared), Id, and Document (populated for Inserted and full-document Updated; null for Removed / Cleared / property-level updates)
Feature
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 overloads
Feature
Global query filters (AddQueryFilter<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 Filters
Feature
MongoDB Upsert performs RFC 7396 deep merge in C# with recursive null stripping, matching CosmosDB / LiteDB / IndexedDB semantics
Feature
MongoDB RunInTransaction 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 transactions
Feature
DuckDB SetProperty 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 RemoveProperty
Feature
DuckDB Query<T>(string) / QueryStream<T>(string) raw SQL parity with the other SQL providers (use json_extract_string(Data, '$.path'))
Feature
Provider matrix in Provider Reference updated with DuckDB and MongoDB columns covering storage type, raw SQL support, predicate translation, deep merge, spatial, backup, and transactions
Fix
SQL Server 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] syntax
Fix
Upsert deep-merge was shallow on PostgreSQL and SQL Server — Postgres used the jsonb || 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 semantics
Fix
Null-stripping was shallow across all providers — when an Upsert patch contained a nested object whose other properties were null (e.g. new 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 Server
Fix
MySQL — DropIndexAsync emitted DROP INDEX {name};, which is invalid in MySQL. Now emits proper drop index on table
Feature
Unit of Work — new 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 Work
Feature
Shiny.DocumentDb.IndexedDb is now 100% AOT/reflection-free. The JS interop layer was rewritten to use [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 reflection
Feature
Internal source-generated JsonSerializerContext (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 unchanged
Feature
Module loading switched from IJSRuntime.InvokeAsync<IJSObjectReference>("import", ...) to JSHost.ImportAsync(...). No app-side changes required
Fix
IndexedDbDocumentStore 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 reflection
Fix
Query operations (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>
Fix
SQLite — table identifiers are now properly quoted in all generated DDL/DML. Mapping a type whose name collides with a SQL reserved word (e.g. Order, Group, User) no longer produces syntax error at table creation or insert time
Backup removed from IDocumentStore interface — now available only on concrete types: SqliteDocumentStore.Backup(), SqlCipherDocumentStore.Backup(), and LiteDbDocumentStore.Backup()
Provider-specific DI extension methods removed (AddSqliteDocumentStore, AddSqlCipherDocumentStore, AddSqlServerDocumentStore, AddMySqlDocumentStore, AddPostgreSqlDocumentStore, AddLiteDbDocumentStore, AddCosmosDbDocumentStore, AddIndexedDbDocumentStore). Use AddDocumentStore from Shiny.DocumentDb.Extensions.DependencyInjection instead
Feature
Named/keyed document store support — AddDocumentStore("name", opts => ...) registers stores as .NET keyed singletons. Inject with [FromKeyedServices("name")] or resolve dynamically via IDocumentStoreProvider.GetStore("name")
Feature
Multi-tenancy support — two isolation strategies via Shiny.DocumentDb.Extensions.DependencyInjection: shared-table (single database with automatic TenantId column filtering) and tenant-per-database (separate database per tenant via lazy factory)
Feature
ITenantResolver interface — implement to provide the current tenant ID. Used by both multi-tenancy strategies to auto-resolve tenant context per request
Feature
AddDocumentStore(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 tenant
Feature
AddMultiTenantDocumentStore(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 automatically
Feature
TenantIdAccessor 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 automatically
Feature
New Shiny.DocumentDb.IndexedDb package — IndexedDB provider for Blazor WebAssembly with IndexedDbDocumentStore. Zero native dependencies, persists to the browser’s IndexedDB via JS interop
Feature
SQLite WASM compatibility — SqliteDatabaseProvider now skips WAL pragma on OperatingSystem.IsBrowser(), spatial R*Tree is disabled in WASM, and Backup() is marked [UnsupportedOSPlatform("browser")]
Feature
New Shiny.DocumentDb.LiteDb package — LiteDB provider with LiteDbDocumentStore
Feature
New Shiny.DocumentDb.CosmosDb package — Azure Cosmos DB provider with CosmosDbDocumentStore
Feature
Spatial/geo query support — WithinRadius, WithinBoundingBox, and NearestNeighbors methods on IDocumentStore with default NotSupportedException for unsupported providers
Feature
GeoPoint readonly record struct — represents a WGS84 coordinate, serializes as GeoJSON {"type":"Point","coordinates":[lng,lat]}
Feature
GeoBoundingBox readonly record struct for area-based spatial queries
Feature
SpatialResult<T> wrapper — returns documents with computed DistanceMeters from the query center point
Feature
MapSpatialProperty<T> on DocumentStoreOptions — register which GeoPoint property to use for spatial indexing per document type
Feature
SQLite spatial support via R*Tree virtual tables — automatic sidecar table creation and CRUD sync for spatial-indexed documents
Feature
CosmosDB spatial support via native ST_DISTANCE and ST_WITHIN GeoJSON queries with automatic spatial index policy
Feature
SupportsSpatial property on IDocumentStore — check if the current provider supports spatial queries at runtime
Feature
SqliteDocumentStore.ClearAllAsync() — deletes all documents across all tables in the SQLite database, including spatial sidecar tables
Feature
Optimistic concurrency via document-level version properties — MapVersionProperty<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 required
Feature
AOT-safe MapVersionProperty<T> overload — MapVersionProperty<T>(string propertyName, Func<T, int> getter, Action<T, int> setter) for trimming-safe deployments
Feature
ConcurrencyException — new exception type with TypeName, DocumentId, ExpectedVersion, and ActualVersion properties for diagnosing version conflicts
Feature
New Shiny.DocumentDb.Extensions.AI package — exposes IDocumentStore operations as Microsoft.Extensions.AI tool functions for LLM agents
Feature
AddDocumentStoreAITools DI extension — opt-in registration of document types with per-type capability flags (ReadOnly, All, or individual Get/Query/Count/Aggregate/Insert/Update/Delete)
Feature
Seven AI tool functions generated per type (when using All): get_by_id, query, count, aggregate, insert, update, delete
Feature
Structured JSON filter expressions with and/or/not combinators and leaf comparisons (eq, ne, gt, gte, lt, lte, contains, startsWith, in) — translated to LINQ expressions at runtime
Feature
Per-type builder API: Description(), Property() description overrides, AllowProperties() / IgnoreProperties() for field visibility control, MaxPageSize() to cap query results
Feature
AOT-safe — all tool schemas and serialization use JsonTypeInfo<T> from source-generated JSON contexts
Feature
Aggregate tool supports count, sum, min, max, avg functions with optional structured filters
Feature
DocumentStoreAITools wrapper class — resolve from DI and pass .Tools to IChatClient / ChatOptions.Tools
Feature
GitHub Copilot sample app demonstrating interactive document management via LLM chat
Feature
New Shiny.DocumentDb.Sqlite.SqlCipher package — encrypted SQLite via SQLCipher with a separate native bundle, no changes to the existing Shiny.DocumentDb.Sqlite package
Feature
SqlCipherDatabaseProvider(filePath, password) — explicit file path and password parameters so users know exactly what is required
Feature
SqlCipherDocumentStore convenience wrapper and AddSqlCipherDocumentStore DI extension for quick setup
Feature
RekeyAsync extension method on IDocumentStore — change the encryption key of an existing SQLCipher database via PRAGMA rekey with SQL injection protection
Feature
Backup support for SQLCipher — automatically propagates the encryption password to the backup database
Feature
DocumentStore.DatabaseProvider public property — exposes the underlying IDatabaseProvider for extension methods
Removed SystemTextJsonPatch dependency — replaced with built-in AOT-compatible JsonPatchDocument<T> and JsonPatchOperation types that use JSON DOM manipulation instead of reflection
JsonPatchDocument<T>.ApplyTo() now returns a new T instead of mutating the target in place — var patched = patch.ApplyTo(original)
Feature
New JsonPatchOperation immutable type with static factory methods: Add, Replace, Remove, Copy, Move, Test
Feature
New JsonPatchDocument<T> with AOT-safe overload accepting JsonTypeInfo<T>patch.ApplyTo(target, MyJsonContext.Default.MyType)
Feature
BatchInsert<T> now uses multi-row INSERT statements chunked into batches of 500 rows, significantly reducing database round-trips — especially impactful for PostgreSQL
Package renamed from Shiny.SqliteDocumentDb to Shiny.DocumentDb with separate provider packages: Shiny.DocumentDb.Sqlite, Shiny.DocumentDb.SqlServer, Shiny.DocumentDb.MySql, Shiny.DocumentDb.PostgreSql
ConnectionString removed from DocumentStoreOptions — replaced by required IDatabaseProvider DatabaseProvider. The connection string is now passed to each provider’s constructor
DI extensions bundled into each provider package — no separate Shiny.SqliteDocumentDb.Extensions.DependencyInjection package
SqliteDocumentStore moved to Shiny.DocumentDb.Sqlite namespace. Base class is now DocumentStore in Shiny.DocumentDb
Feature
SQL Server provider via Shiny.DocumentDb.SqlServer with AddSqlServerDocumentStore DI extension
Feature
MySQL provider via Shiny.DocumentDb.MySql with AddMySqlDocumentStore DI extension
Feature
PostgreSQL provider via Shiny.DocumentDb.PostgreSql with AddPostgreSqlDocumentStore DI extension
Feature
Provider-agnostic IDatabaseProvider interface — swap database backends without changing application code
DI extensions moved to separate Shiny.SqliteDocumentDb.Extensions.DependencyInjection package — the core library no longer depends on Microsoft.Extensions.DependencyInjection.Abstractions
Feature
Convenience constructor — new SqliteDocumentStore("Data Source=mydata.db") for quick setup without options
Feature
Configurable default table name via DocumentStoreOptions.TableName (defaults to "documents")
Feature
Table-per-type mapping — MapTypeToTable<T>() gives a document type its own dedicated SQLite table with lazy creation on first use
Feature
Auto-derived or explicit table names — MapTypeToTable<T>() derives from the type name, MapTypeToTable<T>(string) uses an explicit name
Feature
Duplicate table name protection — mapping two types to the same custom table throws InvalidOperationException
Feature
Custom Id property mapping — MapTypeToTable<T>("table", x => x.MyProperty) uses an alternate property as the document Id instead of the default Id
Feature
Fluent options API — all MapTypeToTable overloads return DocumentStoreOptions for chaining
Feature
Document diffing via GetDiff<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 SystemTextJsonPatch
Feature
All new features are fully AOT-safe — type names and Id property names are resolved at registration time, not at runtime
Feature
Batch insert via BatchInsert<T>(IEnumerable<T>) — inserts a collection in a single transaction with prepared command reuse, auto-generates IDs, and rolls back atomically on failure
Feature
Schema-free JSON document storage on top of SQLite
Feature
Mandatory typed 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 it
Feature
Auto-generation of Ids on Insert: GuidGuid.NewGuid(), int/longMAX(CAST(Id AS INTEGER)) + 1 per TypeName. String Ids must be set explicitly — Insert throws for default string Ids
Feature
LINQ expression queries translated to json_extract SQL with support for equality, comparisons, logical operators, null checks, string methods, nested properties, and collection queries
Feature
Fluent query builder (IDocumentQuery) — chain .Where(), .OrderBy(), .OrderByDescending(), .GroupBy(), .Paginate(), .Select() and terminate with .ToList(), .ToAsyncEnumerable(), .Count(), .Any(), .ExecuteDelete(), .ExecuteUpdate(), .Max(), .Min(), .Sum(), .Average()
Feature
Pagination via .Paginate(offset, take) — translates to SQL LIMIT/OFFSET
Feature
Expression-based ordering — .OrderBy(u => u.Age) and .OrderByDescending(u => u.Name) on the fluent query builder
Feature
SQL-level projections via .Select() using json_object for extracting specific fields without full deserialization
Feature
IAsyncEnumerable streaming via .ToAsyncEnumerable() — yield results one-at-a-time without buffering
Feature
Expression-based JSON indexes for up to 30x faster queries on indexed properties
Feature
Full AOT and trimming support — all JsonTypeInfo parameters are optional and auto-resolve from configured JsonSerializerContext
Feature
Scalar aggregates: .Max(), .Min(), .Sum(), .Average() as terminal methods on the query builder
Feature
Aggregate projections with automatic GROUP BY via Sql.Count(), Sql.Max(), Sql.Min(), Sql.Sum(), Sql.Avg() marker methods
Feature
Collection-level aggregates in projections: Sum, Min, Max, Average on child collections (e.g. o.Lines.Sum(l => l.Quantity))
Feature
Explicit Insert / Update / Upsert API — Insert throws on duplicate Ids, Update throws if not found, Upsert deep-merges via json_patch
Feature
SetProperty — update a single scalar JSON field via json_set without deserializing the document. Supports nested paths
Feature
RemoveProperty — strip a field from the stored JSON via json_remove. Works on any property type
Feature
Typed Id lookups — Get, Remove, SetProperty, and RemoveProperty accept the Id as object (Guid, int, long, or string). Unsupported types throw ArgumentException
Feature
Bulk delete via query builder — .Where(predicate).ExecuteDelete() returns count of deleted documents
Feature
Bulk update via query builder — .Where(predicate).ExecuteUpdate(property, value) updates a property on matching documents via json_set() and returns count updated
Feature
Transactions with automatic commit/rollback via RunInTransaction
Feature
Hot backup via store.Backup(path) — copies the database to a file using the SQLite Online Backup API while the store remains usable
Feature
Dependency injection registration via AddSqliteDocumentStore
Feature
Configurable type name resolution (ShortName or FullName)
Feature
UseReflectionFallback option for strict AOT enforcement
Feature
SQL logging callback via DocumentStoreOptions.Logging
Feature
Raw SQL query and streaming support via store.Query(whereClause) and store.QueryStream(whereClause)