Skip to content

Container-scoped projections work with live aggregation and are validated - #5134

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/5095-scoped-projections
Aug 3, 2026
Merged

Container-scoped projections work with live aggregation and are validated#5134
jeremydmiller merged 1 commit into
masterfrom
fix/5095-scoped-projections

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

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 AddProjectionWithServices with a Scoped or Transient lifetime is wrapped in ScopedAggregationWrapper, and two things went wrong as a result.

Native aggregation silently ran the wrong thing. The wrapper doesn't implement IAggregatorSource, so AggregatorFor<T>() never found it and fell through to conventional aggregation built off the aggregate type. AggregateStreamAsync<T> and RebuildSingleStreamAsync<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 Register overloads in SingleStreamProjection / MultiStreamProjection, plus CompositeProjection, now call ScopedAggregationWrapper.Build(...) instead of closing typeof(ScopedAggregationWrapper<,,,,>) themselves. That 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 instead of a reflection wrapper.

Multi-stream deliberately keeps the general wrapper: only single-stream projections implement IAggregator, and exposing multi-stream to AggregatorFor would be new behavior the Singleton path doesn't have.

Test

Bug_5095_scoped_projection_live_aggregation is a 3×3 matrix — {live aggregation, single stream rebuild, invalid-projection validation} × {Singleton, Scoped, Transient}.

The aggregate deliberately has no conventional Create/Apply methods, so the conventional fallback can't accidentally produce a correct-looking answer; the projection's Evolve (which multiplies by an injected factor) is the only thing that can build it.

before after
Repro matrix 3 passed / 6 failed 9 passed

Every pre-fix failure was Scoped or Transient; every Singleton case passed throughout.

Regression sweep (net9.0)

ContainerScopedProjectionTests 80/80 · EventSourcingTests 1614/1614 · DaemonTests 267/267 · full Marten.slnx builds clean. AggregatorFor is on the hot path for all live aggregation, so the two large suites are the meaningful signal here.

🤖 Generated with Claude Code

…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
jeremydmiller marked this pull request as ready for review August 3, 2026 13:07
@jeremydmiller
jeremydmiller force-pushed the fix/5095-scoped-projections branch from 55b5fde to 0b1f7d0 Compare August 3, 2026 13:07
@jeremydmiller

Copy link
Copy Markdown
Member Author

Rebased onto master now that JasperFx.Events 2.38.0 is published and #5135 has landed. The Directory.Packages.props pin edit dropped out as redundant — master already carries 2.38.0, including the changelog entry for #617.

Re-verified against the published package rather than the local pack this was originally proven with: full Marten.slnx builds clean and ContainerScopedProjectionTests is 80/80 on net9.0, including all 9 cases of the #5095 repro matrix. Out of draft.

🤖 Generated with Claude Code

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.

Service-injected scoped/transient projections are not validated or usable by live aggregation / single stream rebuilds

1 participant