A composite projection's rebuild tears down its members by looping composite.AllProjections() and reading each member's own Options.CleanUps. Two of the four ways to register a member produce a source with a fresh, empty AsyncOptions, so those members' documents are never deleted — and the rebuild then writes into surviving rows.
Distinct from #5169, which is about the test harness's wipe. This one corrupts real rebuilds.
The teardown path
JasperFxAsyncDaemon.cs:1539 → DocumentStore.EventStore.cs:237-249:
if (source is CompositeProjection composite)
{
foreach (var leafSource in composite.AllProjections())
{
teardownProjectionStorage(leafSource, session);
}
...
}
and teardownProjectionStorage (DocumentStore.EventStore.cs:364-389) does source.Options.Teardown(session), which iterates Options.CleanUps (AsyncOptionsExtensions.cs:10-24).
So a member contributes a TRUNCATE only if its own Options.CleanUps is populated.
Where it breaks
| registration |
member source type |
Options.CleanUps |
Snapshot<T>(stage) |
SingleStreamProjection<T,TId> |
populated — JasperFxAggregationProjectionBase ctor :58 calls DeleteViewTypeOnTeardown<TDoc>() |
Add(IProjectionSource, stage) |
as supplied |
populated if the supplied source populates it |
AddProjectionWithServices<TProjection>(...) |
CompositeProjectionWithServicesSource<T> |
empty |
Add(IProjection, stage) |
CompositeIProjectionSource |
empty |
Both wrappers are ProjectionBase subclasses that get a default AsyncOptions and never populate it. CompositeProjectionWithServicesSource<T> calls RegisterPublishedType(aggregateType) (CompositeProjection.cs:189) but never DeleteViewTypeOnTeardown, and — unlike ProjectionWrapper (ProjectionWrapper.cs:51) and ScopedProjectionWrapper (ScopedProjectionWrapper.cs:53) — never calls replaceOptions(source.Options) to adopt the inner projection's options.
So teardownProjectionStorage runs against those leaves and queues nothing. Progression rows and dead-letter rows are still deleted, so the rebuild restarts from zero against a table that still holds the previous run's documents.
Second defect in the same branch
The composite's own Options are never applied. The if branch never calls teardownProjectionStorage(composite, …), so:
composite.Options.DeleteViewTypeOnTeardown<T>() has no effect on rebuild;
composite.Options.TeardownDataOnRebuild = false is never consulted for the composite (only per-member).
Both silently. This matters for #5169: the fix proposed there populates the composite's StorageTypes/CleanUps from its members, which would leave the harness wipe honoring those lists while rebuild continues to ignore them. Worth resolving together.
The per-tenant twin has the same shape — DocumentStore.EventStore.cs:326-336 + teardownProjectionStorageForTenant at :355-362.
Suggested fix
- Have
CompositeProjectionWithServicesSource<T> and CompositeIProjectionSource adopt the wrapped projection's options via replaceOptions(...), matching ProjectionWrapper/ScopedProjectionWrapper. For a raw IProjection with no options of its own, register the published types for teardown.
- Decide whether the composite's own
Options.CleanUps / TeardownDataOnRebuild should apply, and either honor them or make setting them an error rather than a silent no-op.
A general "sources this projection owns" member on IProjectionSource (composite yields members, everything else yields itself) would let the rebuild teardown and the #5169 harness wipe share one traversal and would delete the is CompositeProjection type test here.
A composite projection's rebuild tears down its members by looping
composite.AllProjections()and reading each member's ownOptions.CleanUps. Two of the four ways to register a member produce a source with a fresh, emptyAsyncOptions, so those members' documents are never deleted — and the rebuild then writes into surviving rows.Distinct from #5169, which is about the test harness's wipe. This one corrupts real rebuilds.
The teardown path
JasperFxAsyncDaemon.cs:1539→DocumentStore.EventStore.cs:237-249:and
teardownProjectionStorage(DocumentStore.EventStore.cs:364-389) doessource.Options.Teardown(session), which iteratesOptions.CleanUps(AsyncOptionsExtensions.cs:10-24).So a member contributes a
TRUNCATEonly if its ownOptions.CleanUpsis populated.Where it breaks
Options.CleanUpsSnapshot<T>(stage)SingleStreamProjection<T,TId>JasperFxAggregationProjectionBasector:58callsDeleteViewTypeOnTeardown<TDoc>()Add(IProjectionSource, stage)AddProjectionWithServices<TProjection>(...)CompositeProjectionWithServicesSource<T>Add(IProjection, stage)CompositeIProjectionSourceBoth wrappers are
ProjectionBasesubclasses that get a defaultAsyncOptionsand never populate it.CompositeProjectionWithServicesSource<T>callsRegisterPublishedType(aggregateType)(CompositeProjection.cs:189) but neverDeleteViewTypeOnTeardown, and — unlikeProjectionWrapper(ProjectionWrapper.cs:51) andScopedProjectionWrapper(ScopedProjectionWrapper.cs:53) — never callsreplaceOptions(source.Options)to adopt the inner projection's options.So
teardownProjectionStorageruns against those leaves and queues nothing. Progression rows and dead-letter rows are still deleted, so the rebuild restarts from zero against a table that still holds the previous run's documents.Second defect in the same branch
The composite's own
Optionsare never applied. Theifbranch never callsteardownProjectionStorage(composite, …), so:composite.Options.DeleteViewTypeOnTeardown<T>()has no effect on rebuild;composite.Options.TeardownDataOnRebuild = falseis never consulted for the composite (only per-member).Both silently. This matters for #5169: the fix proposed there populates the composite's
StorageTypes/CleanUpsfrom its members, which would leave the harness wipe honoring those lists while rebuild continues to ignore them. Worth resolving together.The per-tenant twin has the same shape —
DocumentStore.EventStore.cs:326-336+teardownProjectionStorageForTenantat:355-362.Suggested fix
CompositeProjectionWithServicesSource<T>andCompositeIProjectionSourceadopt the wrapped projection's options viareplaceOptions(...), matchingProjectionWrapper/ScopedProjectionWrapper. For a rawIProjectionwith no options of its own, register the published types for teardown.Options.CleanUps/TeardownDataOnRebuildshould apply, and either honor them or make setting them an error rather than a silent no-op.A general "sources this projection owns" member on
IProjectionSource(composite yields members, everything else yields itself) would let the rebuild teardown and the #5169 harness wipe share one traversal and would delete theis CompositeProjectiontype test here.