Two ways to move persistence out of the per-Provider recursion: build the whole graph in memory across several calls, then insert it once, in dependency order, regardless of which Provider originally generated each record.
Bundle accounts = await new RecordProvider<Account>(lookup)
.SetInsertMode(InsertMode.Deferred)
.SetQuantityPerTemplate(3)
.SupplyBundle();
Bundle contacts = await new RecordProvider<Contact>(lookup)
.SetInclusivity(InsertInclusivity.Required)
.SetInsertMode(InsertMode.Deferred)
.SupplyBundle();
await DeferredInserter.Flush(gateway); // one pass, in dependency order, through the given IPersistenceGatewayDeferred generates exactly like Never — no Ids — but registers every
record with DeferredInserter's static registry instead. Flush(gateway)
resolves the whole registered set in dependency order and inserts it through
gateway; call Flush() with no gateway and it throws NotSupportedException
rather than silently doing nothing.
- A test that never calls
Flush()getsNeversemantics — no surprise behaviour. - A failed
Flush()does not silently lose what was registered — the registry only clears after a successful insert, which never happens here.
The piece that is fully usable is flattening a deferred graph and running its
value resolution — including
CopyFromDescendantExpression
— without ever trying to insert:
DeferredInsertBuffer graph = DeferredInsertBuffer.Flatten(bundle);
// graph.Records() / graph.ParentLinks() - the flattened graph, up-flow values already resolved
graph.ResolveAll(InsertMode.Mock); // assigns mock Ids in dependency order, same algorithm Now would useDeferredInsertBuffer and DepthBatchedInserter (below) live in
Net.NowhereAtAll.Xfty.Persistence and are the lower-level pieces
RecordProvider builds on. Reach for them directly when a test wants to prove
the graph is well-formed (parent-before-child ordering, shared ancestors
collapsed to one row, up-flow values resolved) without a working Now.
By default Now would run one insert per Provider. .DepthBatched() collapses
that to one pass per dependency depth:
await new RecordProvider<Case>(lookup)
.SetInclusivity(InsertInclusivity.Required)
.SetInsertMode(InsertMode.Now)
.DepthBatched()
.SupplyBundle();
.DepthBatched()only changes anything when combined withInsertMode.Nowand a configured.SetPersistenceGateway(...)- with any other insert mode, or with no gateway,RecordProviderignores it (it is wired to the same condition that decides whether to flush a deferred graph). With both in place it genuinely runs onegateway.Insert(...)call per dependency depth instead of one per Provider - seeXfty.Test/Persistence/PersistenceGatewayTest.csandXfty.EntityFrameworkCore.Test/SqliteNowPersistenceTest.cs(orXfty.EntityFramework6.Test/SqliteNowPersistenceTest.csfor classic EF6) for the proof against a mock and a real database respectively. The underlying layering algorithm is also proven directly againstDepthBatchedInserter.ResolveAll(records, parentLinks, InsertMode.Mock)- seeXfty.Test/Persistence/DepthBatchedInserterTest.cs.
- Shared ancestors and
CopyFromDescendantExpressionvalues both resolve correctly ahead of the batched step — the whole graph exists in memory first. - A lookup cycle (A → B, B → A) cannot be resolved in dependency order and
throws
CyclicGraphException.
See also: insert-modes · advanced/deep-setup-chains
Runnable: DeferredInserterTest, DeferredInsertBufferTest, DepthBatchedInserterTest, PersistenceGatewayTest