Register an EventProjection's published types as teardown targets (#626) - #629
Merged
Merged
Conversation
#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
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #626.
The problem
JasperFxEventProjectionBase's constructor never touchedOptions, so anEventProjectionregistered no teardown targets —Options.CleanUpsandOptions.StorageTypesstayed empty for the projection's whole lifetime unless the author hand-calledOptions.DeleteViewTypeOnTeardown<T>().JasperFxAggregationProjectionBasehas always done it for its singleTDoc, 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:
Options.Teardown(session)iteratesCleanUps, so a rebuild deleted the progression row and then re-projected into a table still holding the previous run's documents.ProjectionScenarioharness wipe —All.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: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 (viaProjectionGraph) sees everything.Registration is idempotent — a type the author registered by hand, or a second assembly pass, is skipped rather than duplicated.
Opt-out
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 onStorageTypes: sinceDeleteViewTypeOnTeardownwrites to both lists, theProjectionScenariowipe starts working as a side effect. Desirable here, and now deliberate rather than incidental.Tests
EventProjectionTeardownTestsinsrc/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) andEventStoreTests(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