Skip to content

Register an EventProjection's published types as teardown targets (#626) - #629

Merged
jeremydmiller merged 1 commit into
mainfrom
gh/626-event-projection-teardown
Aug 4, 2026
Merged

Register an EventProjection's published types as teardown targets (#626)#629
jeremydmiller merged 1 commit into
mainfrom
gh/626-event-projection-teardown

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #626.

The problem

JasperFxEventProjectionBase's constructor never touched Options, so an EventProjection registered no teardown targets — Options.CleanUps and Options.StorageTypes stayed empty for the projection's whole lifetime unless the author hand-called Options.DeleteViewTypeOnTeardown<T>(). JasperFxAggregationProjectionBase has always done it for its single TDoc, and nothing in the API surface signalled that the two families differed.

Everything that derives "what does this projection own" from those lists therefore silently did nothing for event projections:

  • Rebuild teardownOptions.Teardown(session) iterates CleanUps, so a rebuild deleted the progression row and then re-projected into a table still holding the previous run's documents.
  • Per-tenant progression deletion — same path, per tenant.
  • The ProjectionScenario harness wipeAll.SelectMany(x => x.Options.StorageTypes), identical in Marten and Polecat, never wiped after an event projection.

It was already known and worked around by hand rather than fixed: a Marten test carries a comment explaining the gap followed by an explicit Options.DeleteViewTypeOnTeardown<LegLog>().

The fix

Published types now default to teardown targets — registered in AssembleAndAssertValidity(), not the constructor, for two reasons:

  1. The source generator emits its RegisterPublishedType(typeof(...)) calls into the subclass constructor, which runs after the base one, so a constructor-time pass can't see the complete set. Assembly time (via ProjectionGraph) sees everything.
  2. Registering at assembly time lets a subclass constructor turn the behavior off before it happens.

Registration is idempotent — a type the author registered by hand, or a second assembly pass, is skipped rather than duplicated.

Opt-out

public bool DeletePublishedTypesOnTeardown { get; set; } = true;

For projections writing into storage that must not be truncated on rebuild — an append-only audit table, or documents another projection owns. All-or-nothing by design: turn it off and call Options.DeleteViewTypeOnTeardown<T>() for the types you do want wiped. That recipe has its own test.

Behavior change

This is deliberate and is the point of the issue, but worth stating plainly: rebuilding an event projection now truncates the document types it publishes. Anyone currently relying on the silence — an event projection writing into storage it doesn't own — needs DeletePublishedTypesOnTeardown = false. The issue flagged the same coupling on StorageTypes: since DeleteViewTypeOnTeardown writes to both lists, the ProjectionScenario wipe starts working as a side effect. Desirable here, and now deliberate rather than incidental.

Tests

EventProjectionTeardownTests in src/EventTests/Projections/EventProjectionTests.cs — published types become cleanups + storage types, every published type rather than just the first, a projection publishing nothing registers nothing, the opt-out wins, a hand-registered type isn't duplicated, assembling twice is a no-op, and an explicitly registered type survives the opt-out.

EventTests (724) and EventStoreTests (72) both pass.

Scope note

The issue mentions marten#5169 — the same class of failure for composite projections — as where this was found. That's a separate case (a composite's PublishedTypes() delegates to its member stages) and isn't touched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po

#626)

JasperFxEventProjectionBase's constructor never touched Options, so an
EventProjection registered no teardown targets at all: both Options.CleanUps
and Options.StorageTypes stayed empty for the projection's whole lifetime
unless the author hand-called Options.DeleteViewTypeOnTeardown<T>().
JasperFxAggregationProjectionBase has always done it for its single TDoc, and
nothing in the API surface signalled that the two families differed.

Everything that derives "what does this projection own" from those lists
silently did nothing for event projections: rebuild teardown re-projected into
a table still holding the previous run's documents, per-tenant progression
deletion left the docs behind, and the ProjectionScenario harness wipe (which
reads StorageTypes) never wiped after an event projection. It was already
known and worked around by hand in a Marten test rather than fixed.

Published types now default to teardown targets, registered in
AssembleAndAssertValidity rather than the constructor for two reasons: the
source generator emits its RegisterPublishedType calls into the SUBCLASS
constructor, which runs after the base one, so the constructor cannot see the
complete set; and registering at assembly time lets a subclass constructor set
DeletePublishedTypesOnTeardown = false first. Registration is idempotent, so a
hand-registered type or a second assembly pass does not duplicate.

The opt-out is for projections writing into storage that must not be truncated
on rebuild. It is all-or-nothing by design: turn it off and declare the types
you do want wiped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fuk1GybEEmohFmboJuM4Po
@jeremydmiller
jeremydmiller merged commit b0dc3ce into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EventProjection registers no teardown targets, so rebuilds and the scenario wipe silently skip its documents

1 participant