Skip to content

Composite member projections added via AddProjectionWithServices or raw IProjection are never torn down on rebuild #5175

Description

@jeremydmiller

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:1539DocumentStore.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

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions