Repository navigation
v4.0.1
Added
IRedbScopeSource, implemented by everyIRedbService:CreateScope()opens a scope of the container the
service came from and hands over its service of the same database; the holder disposes the scope.
CanCreateScopesays whether there is a container. For code that holds one service and needs one per unit
of work (redb.Route calls outside an exchange). A container whose scopes resolve another database is refused.IRedbService.BeginAccess(): makes a service the scope lazy loads run on, for a block (UI event
handlers, callbacks outside the flow that resolved the service). See "Lazy loads run on the reader's
scope" under Fixed.IRedbService.LoadLinkedObjectsAsync(items): loads the linked objects of many list items in one
batch on the service's connection and publishes them on the items.RedbServiceConfiguration.LazyLoadWithoutScope(Refuseby default, orFreshScope): what a lazy
load does when no live redb scope reads it.ILazyPropsLoader.CacheDomain(a default interface member): the database a scope-bound loader reads.- The native SQLite extension is versioned like the PostgreSQL and SQL Server modules:
InitializeAsync
asks the loadedredbsqlitelibrary forpvt_module_version()and refuses a build of another version,
naming the file (SqliteDialect.Query_PvtRequiredVersion(), now 0.6.6). A stale library next to the
application loaded silently and answered in an old shape.
Fixed
-
The README of the
redb.Templatespackage described the options of an older template:--dbwithout
sqlite, and defaults of PostgreSQL and Free. The template defaults to SQLite and Pro; the README now says so
and lists what the generated app does. -
RedbSchemaOutdatedExceptionnamed a command theredbtool does not have (redb schema upgrade-script).
It now namesredb schema --upgrade --provider <name>. -
Work inside a
TransactionScopenow belongs to that transaction on MSSQL, PostgreSQL and SQLite
(a redb.Route.Transacted()block among others). Inside a scope redb used to write past the
transaction or fail:- MSSQL: a speculative
ROLLBACKsent right after a connection opened detached it from the scope.
Raw SQL throughredb.Contextautocommitted and survived an abandoned scope, andSaveAsync
failed with "Cannot issue SAVE TRANSACTION when there is no active transaction". - PostgreSQL: a connection opened before the scope stayed out of it (
25P01 SAVEPOINT can only be used in transaction blocks), and a second connection (the key generator, another DI scope)
aborted the commit (55000 prepared transactions are disabled). - SQLite: the driver never takes part in
System.Transactions, so everything autocommitted
without an error.
Now the transaction holds the connection. Every redb connection wrapper of the same database, in
every DI scope, runs its commands on one connection per ambient transaction
(AmbientConnectionRegistry). MSSQL and PostgreSQL open it inside the transaction and enlist. On
SQLite redb runsBEGIN IMMEDIATEon it and commits or rolls it back with the scope, so the
database write lock is held until the scope ends. Inside a scope the key generators take keys on
that connection (a rollback leaves a gap in the sequence, never a duplicate), and the background
key refill no longer inherits the caller's transaction. A lazy load runs on the reader's scope and
so inside its transaction; only a load opened byLazyLoadWithoutScope = FreshScopereads committed
state outside it. The speculativeROLLBACKis gone: the SqlClient pool resets a returned session itself.Behaviour to know:
- Parallel branches in one transaction are not supported. Two commands at the same time on the
transaction's connection (dependent clones, a parallel split) are refused with
InvalidOperationException. Run the branches sequentially, or give each branch a transaction of
its own. - A command after the transaction ended (rolled back because a branch failed, or timed out) is
refused; it never runs outside the transaction. - An explicit redb transaction begun before the scope keeps its own connection, and
BeginTransactionAsyncinside a scope is still refused. - Two redb configurations of the same database that differ in connection parameters or session
settings (lazy references, collation, case folding) do not share a transaction's connection: the
first command of the second one is refused withInvalidOperationException, instead of running on
a connection set up with the other configuration's settings. A connector with its own connection
(for example redb.Route'ssql:) to the same database in the same transaction is a second
connection: SQL Server refuses it, PostgreSQL aborts the commit, SQLite writes outside the
transaction. Raw SQL that belongs to the transaction goes throughredb.Context. - Stores that resolve a DI scope of their own now take part in the caller's
TransactionScope.
- MSSQL: a speculative
-
SQLite: disposing scoped services between
TransactionScope.Complete()andDispose()no
longer throws "The current TransactionScope is already complete". -
DeadlockRetryHelperno longer retries inside an ambientTransactionScope. A deadlock victim's
transaction is already rolled back by the server (SQL Server 1205, PostgreSQL 40P01), so running the
command again inside it failed with a second error (25P02, an aborted transaction) that replaced the
deadlock itself. Inside an ambient transaction the original deadlock now leaves at once and the unit of
work (a route's retry, the caller) runs the whole transaction again. Outside one, and under
TransactionScopeOption.Suppress, the retry works as before. -
Loading an object no longer loads the objects behind its list items.
RedbListItem.Objectis
lazy: reading it is a database load. Several walks over a loaded graph read every property of the
list items they met, so each linked list item (IdObjectset) in Props cost a load of its object
that nobody asked for, and on Pro SQLite the load failed ("Error lazy loading Object for ListItem").
The walks were Pro loads of Props with references (substituting the loaded references, nulling
references to deleted objects, completing lazy reference stubs) and the props-cache check for
unsaved edits in nested objects on every cache hit (Free and Pro, point, bulk and sync loads). A
list item is now a leaf for every such walk, in one rule shared by all of them
(LazyReferenceInstaller.IsWalkLeaf). ReadingObjectexplicitly loads it as before. -
Pro: the synchronous
Load<T>builds the same object asLoadAsync, on the calling thread. It
used to fall back to the Free in-database JSON builder (get_object_json): on Pro SQLite, which has no
such function,Load<T>and a first read ofRedbListItem.Objectfailed ("no such function"; hidden
whenever a Free SQLite registration in the same process had loaded the native extension), and on Pro
MSSQL/PostgreSQL the result differed from the async load (list items lost theirIdObject). The
synchronous Props getter of a lazy reference stub ran its load throughTask.Run, holding a
thread-pool thread per touch on top of the blocked caller. Both now run the Pro materializer itself,
with every database call synchronous on the calling thread: noTask.Run, noParallel.ForEach, and
a load that would go asynchronous is refused instead of blocked on. New synchronous members:
IRedbConnection/IRedbContextQuery<T>andExecute,ISchemeSyncProvider.GetSchemeById,
GlobalMetadataCache.ResolveClrType. -
Free: list items in Props keep
IdListandIdObject. The in-database JSON builders (PostgreSQL
module 0.7.11, MSSQL module 0.2.16, SQLite native extension) wrote a list item with the keyidList
and withoutid_object, while the model readsid_listandid_object. Every Free load returned the
list items in Props withIdList = 0andIdObject = null, soRedbListItem.Objecthad nothing to
load. PostgreSQL and SQLite also built the whole linked object for every such item, under a key the
deserializer ignores.Behaviour to know: the raw object JSON (
LoadJsonAsync, a directget_object_jsoncall) now writes a
list item as{"id", "id_list", "value", "alias", "id_object"}.idListis renamed and the nested
objectis gone; load the linked object byid_object. The SQLite extension must be the rebuilt one. -
MSSQL Free: a reference to an object in the trash stays in its collection as null (module 0.2.17).
An array of references lost that element ([kept, gone]loaded as[kept], shifting every index after
it) and a dictionary of references lost the key, because the aggregate skipped the NULL returned for
the trashed target. PostgreSQL, SQLite and Pro already returnednullin its place; MSSQL now does too. -
A list item's linked object loads typed on a node that has not cached its scheme yet. On a fresh
process or another cluster node,RedbListItem.ObjectandGetObjectAsynctook the CLR type from the
metadata cache only and, on a miss, returned an untypedRedbObject<object>built by
get_object_json(on Pro SQLite, which has no such function, the load failed). The scheme is now
resolved by its id, as the hand-out preload already did.RedbServiceBaseimplements
ISchemeSyncProvider.GetSchemeById. -
SQLite Free: a list item's linked object of a scheme without a CLR type loads. The untyped load sent
the PostgreSQL text of theget_object_jsoncall,::textcast included, and SQLite failed to parse
it. The linked-object loaders now take that SQL from the provider's dialect, like every other load. -
Loads no longer read
[RedbIgnore]properties or swallow a getter's exception. The walks over a
loaded graph (lazy-loader installation, the props-cache collector and its check for unsaved edits, the
Pro reference substitution, the lazy reference stub collection) read every public property, including
[RedbIgnore]ones, and ignored whatever a getter threw; the Props hash wrote an empty string for such
a property. They now walk the property set the scheme and the save use: public, not an indexer, not
[RedbIgnore].Behaviour to know: a getter of a stored property that throws now fails the load and the hash, as it
already failed the save. Mark a computed property that is not data with[RedbIgnore]. -
A failed command no longer leaves a transaction behind on its connection (trash review).
When the SQL text of a command opened a transaction itself and then failed (aSAVEPOINTor
BEGINfollowed by an error, or a command timeout or cancel insideBEGIN TRANSACTION),
nothing ever ended that transaction. On SQLite the pooled handle kept the database write lock,
and every writer waited out its busy timeout ("database is locked") until the pool happened to
hand that handle out again. On MSSQL the scope's later statements ran inside it and were rolled
back with it: a save reported success and its row was gone. On PostgreSQL every later command of
the scope failed with 25P02. The PostgreSQL, MSSQL and SQLite connection wrappers now roll such a
transaction back before the exception leaves, unless a transaction of the wrapper or an ambient
TransactionScopeowns the connection; the original exception always propagates. SQLite also
rolls back an unowned transaction before a handle returns to the pool. -
Soft delete on MSSQL:
sp_mark_for_deletionandsp_purge_trashrun with
SET XACT_ABORT ONand open a transaction only when the caller has none (module 0.2.15). A
timeout inside the mark used to leave@@TRANCOUNT = 1on the session. On SQLite the mark and
purge batches no longer open a savepoint in their own text. On every provider the mark and each
purge batch run inside one transaction, joining the caller's. -
Purging a trash container whose objects are referenced (PostgreSQL module 0.7.10, MSSQL
module 0.2.15, SQLite dialect). ARedbObject<T>reference is a foreign key with no ON DELETE
action, so a single referenced object failed the whole batch, the container stayedrunning
and the background worker retried it every 30 minutes, forever. Now a reference held by an
object that is itself in the trash is removed together with the purge, whatever order the
containers are purged in. An object referenced by a live object is skipped and its reference is
never nulled; everything else is purged, the container is markedfailed(no longer claimed
automatically), andPurgeTrashAsyncthrows the newRedbObjectReferencedExceptionnaming the
referenced and the referencing objects. Remove or re-point those references, then purge the
container again. -
Background deletion worker: a claim that fails no longer ends the poll cycle for every
container after it, the scan and each claim use short scopes of their own, and a container that
endsfailedis logged as a warning and not retried. -
An empty database is named as such at start-up.
InitializeAsync()without
ensureCreated: trueon a database that has no redb schema used to fail on the first
start-up query with a raw driver error (relation "_structures" does not exist,Cannot find the object "dbo._structures",no such table: _schemes) that said nothing about how to
proceed. It now throwsRedbSchemaMissingException, whose message names the two ways out -
InitializeAsync(ensureCreated: true)/RedbServiceConfiguration.EnsureCreated = true, or
the script fromGetSchemaScript()for the schema owner - and reminds that an unexpectedly
empty database is usually a wrong connection string. Schema creation stays opt-in by design.
Pinned on all three providers against a throwaway empty database. -
Props cache under load. With
EnablePropsCachea busy process slowed down several times while its
idle database connections grew. Three causes:- Every cache hit checked the object hash and walked the object's loaded graph under one process-wide
lock, so concurrent loads waited for each other with their connections open. The check now runs
outside the lock. - An insert into a full cache sorted all entries under the write lock to evict one of them. A full cache
now evicts the least recently used tenth ofPropsCacheMaxSizeat once, without sorting. - An object cached again kept the lifetime of its first insert: once
PropsCacheTtlhad passed, an
object reloaded periodically missed on every load. A re-cached object now starts a new lifetime, and an
expired entry is dropped when it is read.
The service now passes its logger to the cache. The cache warns, at most once per 10 seconds, when the
objects in use do not fitPropsCacheMaxSize, when checking a hit takes 50 ms or more, and when 100
lookups find a live entry and serve none (typically a read model that does not reproduce the saved
graph). The detached list-item load warning now counts loads per 10 seconds (200) instead of concurrent
loads (20): loads that run one at a time drain the connection pool as well.Behaviour to know: the props cache no longer applies per-user quotas.
UserConfigurationProps.PropsCacheSizeis still stored and merged, but it does not limit the cache; the
only limit isPropsCacheMaxSize. - Every cache hit checked the object hash and walked the object's loaded graph under one process-wide
-
SQLite:
dbVersion,GetDbVersionAsync()anddbSizework. The SQLite service sent the PostgreSQL
text (version(),pg_database_size(current_database())) and failed with "no such function". It now
returnssqlite_version()and the database file size in bytes (page_count * page_size), Free and Pro. -
dbSizeis in bytes on every provider. The contract said megabytes; PostgreSQL returned bytes and
MSSQL kilobytes. The contract now says bytes, and MSSQL multiplies its 8 KB pages by 8192.Behaviour to know: on MSSQL
dbSizeis now 1024 times larger than before. -
A list of references,
List<RedbObject<T>>, is saved the way an array of them is. The save
recognised arrays only and skipped the elements of a list:- a new object in the list was never saved, and the parent's save failed on the foreign key;
- an edit to a loaded object in the list was silently lost when the parent was saved;
- a reference by id got no hash, so the parent's stored hash never matched the loaded one and the
object missed the props cache on every load.
Any generic collection of
RedbObjectelements now counts, like an array.AddNewObjectsAsyncalso
resolves the hashes of references by id before hashing, asSaveAsyncdoes: the objects it created
with references missed the props cache for the same reason.Behaviour to know: saving a parent now saves the loaded objects in its lists too, as it already did
for arrays and single references. -
Lazy loads run on the reader's scope: no hidden connections.
RedbListItem.Objectand the Props
of a reference stub used to open a fresh DI scope, and with it a pooled connection, for every read
wherever the item or stub carried no loader of the right scope. That covered every list item
materialized by Pro (the Pro service took a secondIListProviderinstance from DI, one without
loaders) or from Free JSON, every reference inside an object served from the props cache, and every
read after the loading scope ended. A data object now owns no connection: such a load runs on the live
redb scope of whoever reads it (the scope that resolvedIRedbServicein the current flow, or a block
inusing (redb.BeginAccess())), on that scope's connection and transaction. Where no scope is current
for the reader (a test fixture, a service kept in a field, a UI event handler), the scope that
materialized the object or item answers, as long as it lives. An instance shared by a cache (the props
cache, the list cache) is never bound to that scope, and neither is anything it loads or already carries.Behaviour to know:
- When no scope is current for the reader and the materializing scope has ended, or when a shared
instance of a cache is read with no scope current, the read throws
RedbLazyLoadScopeEndedException, which names the ways out.
RedbServiceConfiguration.LazyLoadWithoutScope = FreshScopeopens a scope per such load instead, with
a rate warning. - Code that reads objects from a cache outside the flow that resolved the service (UI event handlers in
Blazor Server, WebAssembly, MAUI; callbacks) wraps those reads inusing (redb.BeginAccess()). - An object from the props cache, or an item from the list cache, read inside a transaction sees that
transaction's data and does not keep it, because the write behind it may roll back. Before, the read
went to a separate connection and saw committed state. - Two reads of lazy data at the same time on one scope are refused, like any two concurrent commands
on a scope. - An
IListProviderinjected from DI is now the service's ownListProvider.
- When no scope is current for the reader and the materializing scope has ended, or when a shared
-
A service resolved from the root provider lends lazy loads a fresh scope, not its connection. Such
a captive service (provider.GetRequiredService<IRedbService>()in a Program.cs, a service kept for the
life of the process) is the reader for every flow that touches an object of the props cache or a list
of the list cache, and parallel lazy loads of those instances all ran on its one connection: the second
was refused as a concurrent command. Outside a transaction such a load now runs on a service of a fresh
scope of the same container, on a pooled connection; what it reads is committed state, which a shared
instance keeps. Inside the captive service's transaction the connection is the transaction, and a lazy
load stays on it and reads what the transaction wrote, as before. A high rate of such loads is reported,
as forLazyLoadWithoutScope = FreshScope. What a lent scope materializes belongs to the captive reader:
the stubs under it name the captive service as their origin, not the scope that ended with the load, so
they load the same way.RedbServiceProviders.IsRoot(serviceProvider)tells the root
provider of a Microsoft DI container from a scope's. -
Pro honours the Props depth of a query.
WithPropsDepth(n)onQuery<T>()andTreeQuery<T>()was
ignored by the Pro providers of all three databases: Props loaded toDefaultMaxTreeDepth, so references
meant to stay stubs came back loaded, with the whole graph behind them. -
Tree nodes keep
ValueUnique. Tree queries on Free returned nodes whoseValueUniquewas null: the
conversion of an object to a tree node dropped the key, and on PostgreSQL and SQL Server the tree query did
not even select the column. -
A reference without a hash loads its Props. A reference built in code (
new RedbObject<T> { id = ... })
passed toLoadReferencesAsyncstayed without Props on Free while the props cache was on: the cache had no
hash to match it by, and the object was dropped from the load. On Pro the same reference got empty Props
marked as loaded, because Props are assembled by scheme and the reference carried none; its scheme is now
read from the database. -
Regex.IsMatchandRegex.Replacein filters on SQL Server and SQLite. On SQL Server, Free and Pro,
Regex.IsMatchwas dropped from the query without a word and every row came back;Regex.Replacematched
nothing on Free and was refused on Pro. They now translate toREGEXP_LIKEandREGEXP_REPLACE. On SQLite
both failed: the Pro builder emitted PostgreSQL operators, and the Free extension refused them. The provider
now registersredb_regexp,redb_regexp_iandredb_regexp_replace(.NETRegex) on every connection it
opens, and both editions use them.Behaviour to know:
- SQL Server needs version 2025 with compatibility level 170 for these functions; an older server refuses
such a query by the function name. The SQL Server module version is 0.2.18. - A pattern runs on the engine of the database: POSIX on PostgreSQL, RE2 on SQL Server, .NET on SQLite.
- The SQLite Free extension must be rebuilt for every platform.
- SQL Server needs version 2025 with compatibility level 170 for these functions; an older server refuses
-
A tree node is served from the props cache. An object saved as
TreeRedbObject<T>- every
CreateChildAsync, every tree node saved directly - stored a header-only hash: the save recognised the
exact generic typeRedbObject<>and nothing derived from it, so the hash the loaded object recomputes
(header and Props) never matched the stored one and every cache probe of such an object was a miss. The
same test kept a tree node out of the cache after its save and out of the scheme auto-detection.Behaviour to know: a node saved before this fix carries the old hash until its next save; until then it
loads from the database as before, and the next save stores the hash the cache matches. -
A moved object is not served from the props cache with its old parent.
MoveObjectAsyncupdates the
parent outside the save path and drops the object's hash in the database, but left the cached copy in
place; withSkipHashValidationOnCacheChecka load never asks the database and answered with the old
parent until the entry expired. -
Loading objects by a list of ids asks the database once, not once per id, and uses the props cache under
SkipHashValidationOnCacheCheck. The batch used to issue oneSELECT id, hash, schemeper id before
the cache probe, and with the flag it skipped the cache altogether and rematerialized every object. One
query now resolves the hashes and schemes of the whole batch, in both modes. This is the road of the
list-item preload and ofLoadLinkedObjectsAsync. -
The object hash sees sub-second changes and does not depend on the process culture. The canon of a
Props value wasToString(): aDateTimeorDateTimeOffsetlost its sub-second digits, and adouble,
decimal,DateOnlyorTimeOnlytook the decimal separator and date format of the current culture.
Under the ChangeTracking save an object whose hash equals the stored one is not written, so a temporal
property changed within the same second - a heartbeat, a "last seen" - was silently lost; and two nodes
with different cultures hashed one object differently. The canon is now invariant: temporal values in
UTC at millisecond precision (the precision every database keeps), numbers in the invariant culture.
ADateTimehashes by its clock reading whatever itsKind, exactly as it is stored.Behaviour to know: the hash of every object with temporal or floating-point Props changes once. The
first save of such an object after the upgrade runs the full diff (the ChangeTracking shortcut does
not apply) and stores the new hash; the first props-cache probe of it misses. A change below one
millisecond is not a change: the databases do not keep it. -
SQLite: dates before 1899-12-30 and the temporal
MinValues round-trip. The REAL Julian day of a
DateTime,DateTimeOffsetorDateOnlywas computed through the OLE automation date (ToOADate),
which is not linear: its zero stands forDateTime.MinValue, so a default date came back as
1899-12-30, and a day before its epoch is encoded as sign and magnitude, so any time of day before
1899-12-30 moved by a day - on the way in through the Pro writer and every filter value, and on the way
out for rows the native extension wrote through SQLite's ownjulianday(). The day count is now linear
in the tick count, truncated to the millisecond; for every instant from 1899-12-30 on it is bit for bit
the number stored before, so existing rows and equality filters keep matching. -
PostgreSQL: a trash purge no longer fails the container when another purger deleted its batch.
purge_trashmarked the container 'failed' when a batch deleted nothing while objects remained, reading
it as "every remaining object is referenced by a live object". A batch chosen here and deleted meanwhile
by a concurrent purger - the background worker beside a caller'sPurgeTrashAsync, another node -
failed the container with nothing blocking it, the worker stopped claiming it, and the caller got
RedbObjectReferencedExceptionnaming no referrer. 'failed' now means an empty batch; a batch that
deleted nothing leaves the container 'running', and the purge goes on with the next one. PostgreSQL
module 0.7.12, applied byInitializeAsynclike every module upgrade. SQL Server counts the batch it
chose and SQLite chooses and deletes under the write lock, so neither saw the race. -
SQLite: a finished trash container is removed. PostgreSQL and SQL Server remove the container in
the purge function once it holds no objects; the SQLite purge statement ends with its result row and
could not, so every completed container stayed in_objectsfor good, andGetDeletionProgressAsync
kept answering 'completed' for it where the other providers answer null. The purge now removes it
after the last batch. -
RedbListItem.GetObjectAsync(cancellationToken): the token reaches the load. It was accepted and dropped
on the way to the query, so a cancelled reader still ran the scheme lookup and the load. -
A scheme without a CLR type is resolved once per cache domain. Reading the object behind a list item of
such a scheme loaded the scheme again on every read: a list of 200 items was 200 scheme queries. -
The props cache no longer takes a process-wide lock on every hit for the "found but never served"
diagnostic; its counters are lock-free like the other diagnostics. -
Two hosts of one database that differ in
RedbServiceConfiguration.LazyLoadWithoutScopeare reported
with a warning, once per database. The registration of the most recently constructed service wins, so
which policy a lazy load with no live scope got depended on which host constructed a service last. -
An edit made inside a transaction on the lazy reference of a loaded object is saved; the caches
publish committed state only. A props-cache Set marked the loaded graph shared at once, and a shared
instance does not keep what one transaction saw - so inside a transaction the writer's own loaded object
never kept its lazy references:root.Props.Next.Props.Label = ...; SaveAsync(root.Props.Next)saved a
fresh reload and the edit was lost, and every read ofitem.Objectof a cached list was a query. A Set
inside a transaction (an explicit redb transaction or an ambientTransactionScope) now waits for its
commit; until then the instances are the reader's own, and what a rolled-back transaction loaded never
reaches the cache. Behaviour to know: inside one transaction the same object loaded twice is materialized
twice - the cache answers only after the commit. -
A saved object served from the cache loads its hand-made references. The save cached the caller's
graph as it was, and a reference written asnew RedbObject<T> { id = x }carried no loader: a load
served from the cache returned that very stub, and itsPropscame back null - no query, no exception.
The save now gives the stubs of the saved graph the loader of its database before caching, as a load
does. -
A saved graph that is not a tree hashes references before their parents; a new object referenced
twice is saved once. A parent's hash carries "id:hash" of every reference, and the save hashed in the
reverse of a pre-order walk that collects a target shared by two parents once, under the first: the
second parent stored the target's stale hash for ever - a props-cache miss on every load. The same walk
never deduplicated a new object: one instance referenced from two places was collected twice, given an
id twice and inserted twice. -
The connection of an ambient transaction opens under a lock per transaction and database. One
process-wide lock held the first open of every other transaction, on every database, behind a SQLite
BEGIN IMMEDIATEwaiting for its write lock. -
The save answers "is this a reference / a collection of references / a business class" once per type
instead of reflecting over the type's interfaces for every property of every object, and caching a
loaded graph no longer walks every nested object's subtree again to mark it shared - the root's Set
marked the whole graph. -
Saving a reference whose properties were never loaded is refused with
RedbUnloadedReferenceException.SaveAsync(root.Props.Next)whereNextis a stub whose lazy load
the getter did not keep (a shared instance inside a transaction), or a hand-written
new RedbObject<T> { id = x }, saved whatever a fresh load returned - the caller's edits were lost
silently - and cached the unloaded stub, whose validation on the next load was a lazy load inside the
cache probe: a deadlock with the load that probes. The exception names the ways out: load the object
(redb.LoadAsync(id),redb.LoadReferencesAsync(parent, p => p.Reference)), edit and save the loaded
one. Reading thePropsof such a reference still answers null: plain System.Text.Json serialization
of a graph reads the getter, and an exception there would fail every API response carrying a hand-made
reference. A saved graph gives its hand-made references their loader, with or without the props cache.
An object without Props - a scheme with no properties (a flag, a counter: the object lives in its base
fields), an object saved withProps = null- is a loaded object, not a reference: what redb materializes
as a root (LoadAsync, a query, a tree) is loaded by definition, on every provider; the refusal applies to
a stub redb attached a lazy loader to (a depth boundary, a cache) that is still unloaded. -
The props cache never holds or serves an unloaded instance: a save caches loaded objects only, and a
cache hit whose instance has no loaded Props is not served. -
A shared instance keeps what a transaction loads while the transaction has written nothing, and
forgets it if the transaction rolls back. The rule "a shared instance does not keep what one
transaction saw" guarded a narrow hazard - a transaction's own uncommitted write read back through a
shared parent - at the price of the writer's edits in every ordinary flow: an object served from the
cache, or loaded before the transaction, edited through its lazy reference inside the transaction
(root.Props.Next.Props.Label = ...; SaveAsync(root.Props.Next)) saved a fresh reload. While the
transaction has written nothing, what its connection reads is committed state, so the shared instance
keeps it - stubs and list items alike - and drops it if the transaction ends without a commit. Once the
transaction has written (a save, a delete, a tree move, a rawContext.ExecuteAsync, a bulk operation,
a soft delete or purge), what it reads may be its own uncommitted work: the shared instance keeps nothing
of it, and saving such a stub is refused withRedbUnloadedReferenceExceptioninstead of writing a
fresh reload.
Changed
-
The
redb-consoletemplate andredb.ExamplesresolveIRedbServicefrom a scope (provider.CreateAsyncScope())
instead of from the container root: one service is one connection, and a scope per unit of work is the shape
that carries over to hosts with many flows. A console works either way. -
Microsoft.Data.SqlClient7.0.3 (was 5.2.2) inredb.MSSqlandredb.Export;redb.MSSql.Pro
gets it throughredb.MSSql. The 7.0 line supports .NET 8 and newer and shipsnet8.0andnet9.0
builds (net10.0uses thenet9.0one). redb.Route, redb.Tsak and redb.Identity build and test on the
same driver version.Behaviour to know:
- Microsoft Entra ID authentication (
Authentication=Active Directory ...in the connection string)
is no longer in the driver package. An application that uses it adds
Microsoft.Data.SqlClient.Extensions.Azure. The driver no longer bringsAzure.Core,Azure.Identity
andMicrosoft.Identity.Client. redb does not use Entra ID itself. - The driver lines 5.x and 6.x are no longer supported. An application that pins
Microsoft.Data.SqlClientbelow 7.0.3 gets a NU1605 package downgrade error and has to raise the pin. - The driver needs
Microsoft.IdentityModel.*andSystem.IdentityModel.Tokens.Jwt8.16.0 or newer.
A lower pin of those packages in an application is a NU1605 downgrade as well.redb.Licensingreferences
System.IdentityModel.Tokens.Jwt8.16.0 (was 8.0.0), soredb.Core.Pro,redb.Postgres.Proand
redb.SQLite.Probring the same version withoutredb.MSSqlin the graph.
- Microsoft Entra ID authentication (
-
UserConfigurationProps.PropsCacheSizeis obsolete and no longer merged into
EffectiveUserConfiguration: the props cache has one process-wide limit,
RedbServiceConfiguration.PropsCacheMaxSize, and no quota per user. The value is still stored with the
configuration object;EffectiveUserConfiguration.PropsCacheSizereports the process limit for every
user, sys included. -
RedbLazyLoadScopeEndedException: the list item form isForListItem(listItemId, objectId); the
three-argument constructor with itsfromListItemflag is gone. -
ObjectStorageProviderBase.CollectAllObjectsRecursively(protected) takes the set of instances already
collected, next to the set of ids. -
New public helpers:
TransactionCompletion(a provider transaction reports its outcome once; callbacks
registered for it run with whether it committed),CachePublication.AfterCommit(runs a publication
now or once the context's transaction commits).
Removed
RedbListItem.SetGlobalObjectLoader,SetGlobalSyncObjectLoader,IsObjectLoaderAvailable,
AttachObjectLoader,AttachSyncObjectLoaderandHasObjectLoader;IListProvider.LinkedObjectLoader
andLinkedObjectSyncLoader;DetachedLazyPropsLoaderandScopeFallbackLazyPropsLoader; the scope
factory parameter ofGlobalPropsCache.Initialize. These bound items and stubs to one scope, or opened
a fresh scope per load to make up for it (see "Lazy loads run on the reader's scope" above).MemoryRedbObjectCacheconstructor parametersgetUserIdFuncandgetQuotaFunc, its method
GetUserStatistics()and the classUserCacheStats: the props cache has no per-user quotas any more
(see "Props cache under load" above). The quota lookup ran synchronously on every insert, through a
configuration service taken from the scope that initialized redb, and its failures were swallowed.- Unreachable PostgreSQL SQL in the SQLite and Pro builders.
SqliteDialect.Query_HasAncestorTreeSql,
Query_HasAncestorNormalSqlandQuery_HasDescendantSqlnow refuse withNotSupportedException, like the
other search functions SQLite does not have. The Pro SQL builders no longer translateArrayAny,
ArrayEmptyand theArrayCount…comparisons, which no LINQ parser produces; such a comparison is refused
instead of compiling to SQL (on SQLite, to PostgreSQL SQL).