Skip to content

Cover MultiStreamProjection samples and add batch-aware Pattern 4 (#4261)#4263

Merged
jeremydmiller merged 1 commit intomasterfrom
fix/4261-multistream-projection-samples
Apr 17, 2026
Merged

Cover MultiStreamProjection samples and add batch-aware Pattern 4 (#4261)#4263
jeremydmiller merged 1 commit intomasterfrom
fix/4261-multistream-projection-samples

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #4261.

What

The three MultiStreamProjection "aggregate id not on the event" patterns in docs/events/projections/multi-stream-projections.md were backed by src/EventSourcingTests/Projections/MultiStreamProjections/CustomGroupers/grouping_examples_for_unknown_ids.cs — but that file contained only compile-only class declarations for the snippet extractor. No [Fact] methods actually ran those patterns end-to-end, so a real bug in Pattern 2 went unnoticed for a long time (see discussion #3615 and the gist linked from #4261).

This PR:

  1. Adds end-to-end regression tests for all documented patterns under ProjectionLifecycle.Async in Bug_4261_multistream_sample_coverage.cs.
  2. Locks in the Pattern 2 race as a documented limitation — the test asserts the broken behavior so any future engine fix will surface as a failing test.
  3. Introduces Pattern 4 — a batch-aware grouper that scans the current IEnumerable<IEvent> for in-flight link events, falls back to a DB lookup for anything unresolved, and keeps a grouper-instance cache off the hot path.
  4. Rewrites the Pattern 2 doc with a ::: danger block that explicitly names the race, describes the in-memory-events + projection-cache requirement for a safe implementation, and recommends against this pattern.
  5. Adds a Pattern 4 doc section that cross-references from Pattern 2 and shows the full example.

Test results (net8.0 / net9.0 / net10.0)

All 7 new tests pass on all three target frameworks:

  • pattern1_async_same_batch_link_and_usage_is_applied_correctly — ✅
  • pattern1_async_usage_before_link_in_separate_batches_works — ✅
  • pattern2_async_same_batch_loses_usage_event_known_limitation — ✅ (asserts broken behaviour, locked-in regression)
  • pattern3_async_same_batch_is_correct_by_design — ✅
  • pattern4_async_same_batch_link_and_usage_works — ✅
  • pattern4_async_link_in_earlier_batch_then_usage_works — ✅

Notes for the reviewer

  • The Pattern 2 test was originally written to assert the intended-correct behavior and failed. That proved the bug. The assertion was then flipped to ShouldBe(0) with a comment explaining this is intentionally locked-in broken-behavior coverage that should flag any future engine-level fix.
  • Pattern 1 and Pattern 4 both rely on the same inline ExternalAccountLinkProjection. Pattern 1 is safe only because the inline projection commits before the async daemon reads the batch; Pattern 4 is safe regardless of that ordering because it consults the batch first.
  • The grouper-instance cache in Pattern 4 is unbounded by design; the docs note this and suggest LRU eviction or periodic reset for long-running processes where external ids are high-cardinality.
  • Doc snippet anchors were verified with the markdownlint-cli image the CI uses (exit 0 with the same flags).

🤖 Generated with Claude Code

…grouping (#4261)

The three MultiStreamProjection patterns for "aggregate id not on the event"
previously had no end-to-end tests — the snippet source file
grouping_examples_for_unknown_ids.cs held only compile-only declarations.
This change closes that gap and resolves the race described in the issue.

Regression tests (Bug_4261_multistream_sample_coverage.cs) run all four
patterns under ProjectionLifecycle.Async and confirm:
  * Pattern 1 works — the inline lookup projection commits before the async
    daemon reads the batch, so the grouper's DB lookup resolves.
  * Pattern 2 silently drops usage events when the link event and the usage
    event share a single SaveChangesAsync batch. The test is kept as a
    locked-in regression (asserts the broken behaviour) so any future fix
    is visible.
  * Pattern 3 works — the derived event carries the group key directly.
  * Pattern 4 (new) works, including same-batch ordering.

Pattern 4 is the new recommended shape for cross-stream grouping when link
and usage events can appear in the same batch. The grouper scans the batch
for in-flight link events first, then falls back to a DB lookup for anything
unresolved, with a grouper-instance cache keeping repeated lookups off the
hot path.

Docs (docs/events/projections/multi-stream-projections.md):
  * Pattern 2 is now marked "Not recommended" with an explicit danger callout
    describing the race, pointing at the in-memory-events + lookup-cache
    requirement, and deferring to Pattern 4.
  * Pattern 4 section added with full example drawn from the new snippet
    source.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit ba79e4f into master Apr 17, 2026
4 of 6 checks passed
@jeremydmiller jeremydmiller deleted the fix/4261-multistream-projection-samples branch April 17, 2026 19:49
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.

Review MultiStreamProjection samples

1 participant