Container-scoped projections work with live aggregation and are validated - #5134
Merged
Conversation
…ated (#5095) Closes #5095 Consumes the JasperFx.Events fix in #617 and routes Marten's wrapper construction through the new selector. A projection registered via AddProjectionWithServices with a Scoped or Transient lifetime was wrapped in ScopedAggregationWrapper, which does not implement IAggregatorSource -- so AggregatorFor<T>() never found it and fell through to conventional aggregation built off the aggregate type. AggregateStreamAsync and RebuildSingleStreamAsync silently ran something other than the registered projection. The same wrapper also hid projection validation, so an invalid projection that a Singleton registration rejects at startup was accepted when registered Scoped. Marten side is small: the four Register overloads in SingleStreamProjection and MultiStreamProjection, plus CompositeProjection, now call ScopedAggregationWrapper.Build(...) instead of closing typeof(ScopedAggregationWrapper<,,,,>) themselves. The helper picks the single stream wrapper (which can serve live aggregation) or the general one, and unwraps the reflection TargetInvocationException so a configuration error surfaces as the real InvalidProjectionException. Regression test is a 3x3 matrix -- {live aggregation, single stream rebuild, invalid-projection validation} x {Singleton, Scoped, Transient} -- over an aggregate with NO conventional Create/Apply methods, so the conventional fallback cannot produce a correct-looking answer. Verified 3 passed/6 failed before the fix (every failure Scoped or Transient) and 9 passed after. net9.0: ContainerScopedProjectionTests 80/80, EventSourcingTests 1614/1614, DaemonTests 267/267, full Marten.slnx builds clean. BLOCKED: the JasperFx/JasperFx.Events 2.38.0 pin is the *intended* published version -- #617 is open, not published. Verified against a local pack; re-verify against the real package before merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jeremydmiller
marked this pull request as ready for review
August 3, 2026 13:07
jeremydmiller
force-pushed
the
fix/5095-scoped-projections
branch
from
August 3, 2026 13:07
55b5fde to
0b1f7d0
Compare
Member
Author
|
Rebased onto master now that JasperFx.Events 2.38.0 is published and #5135 has landed. The Re-verified against the published package rather than the local pack this was originally proven with: full 🤖 Generated with Claude Code |
This was referenced Aug 4, 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 #5095. Consumes JasperFx/jasperfx#617.
Important
Draft: blocked on JasperFx.Events 2.38.0. The pin here is the intended published version; #617 is open, not published. Everything below was verified against a local pack of that branch and needs re-verification against the real package before this leaves draft.
The bug
A projection registered through
AddProjectionWithServiceswith a Scoped or Transient lifetime is wrapped inScopedAggregationWrapper, and two things went wrong as a result.Native aggregation silently ran the wrong thing. The wrapper doesn't implement
IAggregatorSource, soAggregatorFor<T>()never found it and fell through to conventional aggregation built off the aggregate type.AggregateStreamAsync<T>andRebuildSingleStreamAsync<T>therefore ignored the registered projection's logic entirely — no error, just a different answer. Registering the same projection as a Singleton worked, which is the asymmetry the reporter hit.Validation was hidden. The wrapper's base constructor copied name/version/options off the resolved projection but never called
AssembleAndAssertValidity()on it — only the wrapper was validated. A projection that a Singleton registration rejects at startup was silently accepted when registered Scoped, so switching lifetime later would surface a configuration error that had been there all along.What changed in Marten
Small — the real work is in #617. The four
Registeroverloads inSingleStreamProjection/MultiStreamProjection, plusCompositeProjection, now callScopedAggregationWrapper.Build(...)instead of closingtypeof(ScopedAggregationWrapper<,,,,>)themselves. That helper picks the single-stream wrapper (which can serve live aggregation) or the general one, and unwraps the reflectionTargetInvocationExceptionso a configuration error surfaces as the realInvalidProjectionExceptioninstead of a reflection wrapper.Multi-stream deliberately keeps the general wrapper: only single-stream projections implement
IAggregator, and exposing multi-stream toAggregatorForwould be new behavior the Singleton path doesn't have.Test
Bug_5095_scoped_projection_live_aggregationis a 3×3 matrix — {live aggregation, single stream rebuild, invalid-projection validation} × {Singleton, Scoped, Transient}.The aggregate deliberately has no conventional
Create/Applymethods, so the conventional fallback can't accidentally produce a correct-looking answer; the projection'sEvolve(which multiplies by an injected factor) is the only thing that can build it.Every pre-fix failure was Scoped or Transient; every Singleton case passed throughout.
Regression sweep (net9.0)
ContainerScopedProjectionTests80/80 ·EventSourcingTests1614/1614 ·DaemonTests267/267 · fullMarten.slnxbuilds clean.AggregatorForis on the hot path for all live aggregation, so the two large suites are the meaningful signal here.🤖 Generated with Claude Code