Add ForEntityIds for fan-out enrichment (1-event-to-N-entities)#208
Merged
jeremydmiller merged 1 commit intomainfrom May 7, 2026
Merged
Add ForEntityIds for fan-out enrichment (1-event-to-N-entities)#208jeremydmiller merged 1 commit intomainfrom
jeremydmiller merged 1 commit intomainfrom
Conversation
…to-N-entities) Closes the 1-to-N gap that fell out of JasperFx/marten#4329 — the existing EnrichWith<TEntity>().ForEvent<TEvent>().ForEntityId(selector).AddReferences() chain only models a single referenced entity per event. Real-world projections often need to enrich one event with several upstream entities of the same type (e.g. an event carrying a list of foreign-key ids). Until now the only supported workaround was to drop down to the EnrichUsingEntityQuery escape hatch and write the fan-out by hand. This adds a first-class, declarative variant that mirrors the existing 1-to-1 chain: await group .EnrichWith<Provider>() .ForEvent<ShiftScheduled>() .ForEntityIds(e => e.ProviderIds) // Func<TEvent, IEnumerable<TEntityId>> .AddReferences(); // one References<Provider> per resolved id Implementation: - New EventStep<TEntity, TEvent>.ForEntityIds<TEntityId>(...) returning MultiIdentityStep<TEntity, TEvent, TEntityId>. - Companion ForEntityIdsFromEvent that hands the IEvent<TEvent> wrapper to the selector, matching the pattern of the existing ForEntityIdFromEvent. - MultiIdentityStep mirrors IdentityStep: AddReferences, EnrichAsync, and FetchEntitiesAsync. AddReferences emits one References<TEntity> per resolved id in order; EnrichAsync invokes the callback once per (slice, event, resolved entity) triple. Missing ids are silently skipped. - FetchEntitiesAsync de-duplicates ids before LoadManyAsync so the storage layer is never asked for the same id twice within an enrichment. Tests cover: per-event fan-out via AddReferences, missing-id skipping, storage-side dedupe across events that reference the same id, the empty-ids fast path, and EnrichAsync's per-resolved-entity callback semantics. Refs JasperFx/marten#4329. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
May 7, 2026
Two changes ride this release: 1. Composite-projection cache eviction fix (#206): downstream stages of a composite no longer lose access to upstream in-flight entities when CacheLimitPerTenant is small relative to per-batch fan-out. 2. ForEntityIds (#208): first-class 1-event-to-N-entities enrichment via group.EnrichWith<T>().ForEvent<E>().ForEntityIds(selector).AddReferences(). Refs JasperFx/marten#4329, #206, #208. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
to JasperFx/marten
that referenced
this pull request
May 7, 2026
JasperFx.Events 1.35.0 ships two changes that close out the usability gap @xander-abn surfaced in #4329: 1. Composite-projection cache eviction fix (JasperFx/jasperfx#206) — AppointmentByExternalIdentifier (and similar EnrichWith<UpstreamDoc> chains) no longer drop downstream lookups when the upstream's CacheLimitPerTenant is small relative to per-batch fan-out. 2. ForEntityIds (JasperFx/jasperfx#208) — first-class declarative shape for 1-event-to-N-entities enrichment. This commit: - Bumps JasperFx.Events 1.34.0 → 1.35.0 in Directory.Packages.props. - Adds Bug_4329_fan_out_and_cache_limit with two facts: a fan-out scenario using ForEntityIds (one OrderPlacedWithLineItems event referencing 5 upstream Product documents), and a cache-eviction regression that intentionally sets ProductProjection.CacheLimitPerTenant = 1 and verifies 20 distinct upstream lookups all resolve to the in-flight cache. - Wires the new patterns into composite.md and enrichment.md, removes the now-stale "cache-limit is load-bearing for correctness" caveat, and adds a "Fan-out enrichment with ForEntityIds" section sourced from #region sample_for_entity_ids_fan_out in the new test. Refs #4329, JasperFx/jasperfx#206, JasperFx/jasperfx#208. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Companion API addition for JasperFx/marten#4329. Closes the 1-to-N gap reported in @xander-abn's followup — the existing declarative enrichment chain only models a single referenced entity per event, so a projection that needs to enrich one event with several upstream entities of the same type (e.g. an event carrying a list of foreign-key ids) had to drop down to `EnrichUsingEntityQuery` and write the fan-out by hand.
This adds a first-class declarative variant that mirrors the existing 1-to-1 chain:
```csharp
await group
.EnrichWith()
.ForEvent()
.ForEntityIds(e => e.ProviderIds) // Func<TEvent, IEnumerable>
.AddReferences(); // one References per resolved id
```
What changes
Test plan
5 new unit tests in `SliceGroupTests`:
All 240 `EventTests` pass on net8.0 / net9.0 / net10.0.
Pairs with #206
The cache-eviction correctness fix in JasperFx/jasperfx#206 (just merged) is the prerequisite for using `ForEntityIds` in composite projections at scale — without that fix, a single event referencing more upstream entities than `CacheLimitPerTenant` holds would silently drop the evicted ones onto `LoadManyAsync`, which can't see in-flight upstream writes. Both will ship together in the next `JasperFx.Events` release.
🤖 Generated with Claude Code