Source-generate self-aggregating records from their own declaration (marten#4557)#367
Merged
Merged
Conversation
…ation Self-aggregating types must never have to be `partial`. Classes with conventional Apply/Create were already discovered by Pipeline 1 from their own declaration and got a free-standing evolver + [assembly: GeneratedEvolver]. Records, however, were filtered in IsCandidateClass to Evolve/EvolveAsync only (a fossil of the removed runtime reflection path), so a self-aggregating *record* only ever got an evolver when the generator could see a Snapshot<T>/AggregateStream<T> call site (Pipeline 3). That asymmetry is the root of marten#4557's cross-assembly gap: a record aggregate defined in a domain library but registered in a separate composition-root assembly never had a [GeneratedEvolver] emitted into its own assembly — which is exactly where the runtime (tryUseAssemblyRegisteredEvolver) scans, keyed on typeof(TDoc).Assembly. Fix: treat records identically to classes in the candidate predicate. A self-aggregating record with Apply/Create now gets its evolver emitted from its own declaration, with no Snapshot<T> call site required and without being declared `partial`. ExecuteCombined's existing dedup keeps same-assembly registrations single-emit (Pipeline 1 claims the type; Pipeline 3 skips it), matching how classes already behave. Also corrects the misleading "No source-generated dispatcher found" message in AggregateApplication + the EvolveAsync backstop: drops the blanket "must be partial" claim (only projection subclasses need it), points at the assembly that defines the aggregate as the analyzer/lookup site, notes the generator ships in the Marten package, and removes the dead docs/codegen/aot.md link. SG suite 19/19 (incl. new self_aggregating_record_emits_evolver_from_its_own_declaration _without_partial); EventTests compiles clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
May 25, 2026
Patch release carrying #367 (marten#4557): the source generator now emits a self-aggregating evolver for `record` aggregates from their own declaration (parity with classes — no Snapshot<T> call site and no `partial` required), and the "No source-generated dispatcher found" message is corrected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 25, 2026
This was referenced Jun 1, 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.
Problem
Downstream report JasperFx/marten#4557: a user upgrading 8 → 9 has a self-aggregating immutable
recordwith staticCreate/Apply, registered viaProjections.Snapshot<MyType>(Inline). It throws atDocumentStore.For(...):Root cause
The generator's candidate predicate treats records differently from classes:
Apply/Createis discovered by Pipeline 1 from its own declaration and gets a free-standing evolver +[assembly: GeneratedEvolver(...)].IsCandidateClasstoEvolve/EvolveAsynconly (a fossil of the removed runtime-reflection path), so a self-aggregating record only ever got an evolver when the generator could see aSnapshot<T>/AggregateStream<T>call site (Pipeline 3).That asymmetry is the root of the cross-assembly gap: a record aggregate defined in a domain library but registered in a separate composition-root assembly never has a
[GeneratedEvolver]emitted into its own assembly — which is exactly where the runtime scans (tryUseAssemblyRegisteredEvolverkeys ontypeof(TDoc).Assembly). It also creates the impression that such a record must bepartial, which it must not.Fix
AggregateEvolverGenerator.IsCandidateClass: treat records identically to classes. A self-aggregating record withApply/Createnow gets its evolver emitted from its own declaration — noSnapshot<T>call site required, and never requiringpartial.ExecuteCombined's existing dedup keeps same-assembly registrations single-emit (Pipeline 1 claims the type; Pipeline 3 skips it), exactly as classes already behave.AggregateApplication.MissingDispatcherMessageand theEvolveAsyncbackstop — dropped the blanket "must bepartial" claim (only projection subclasses need it), pointed at the assembly that defines the aggregate as the analyzer/lookup site, noted the generator ships inside the Marten package, and removed the deaddocs/codegen/aot.mdlink.Guiding principle: self-aggregating types (class or record) must never have to be
partial— only projection subclasses do.Validation
JasperFx.Events.SourceGenerator.Tests: 19/19 green, including a newself_aggregating_record_emits_evolver_from_its_own_declaration_without_partial(non-partialrecord, noSnapshot<T>call site → asserts the evolver +[GeneratedEvolver]attribute are emitted, no errors).EventTestscompiles clean (0 errors) under the new generator.Downstream
JasperFx/marten#4557. Marten separately bundles this analyzer in its own NuGet package so package consumers get the generator automatically; once a generator release carries this change, Marten's bundled analyzer picks up the record/cross-assembly fix.
🤖 Generated with Claude Code