Skip to content

Keep the correlation and causation on an event's clones (#1101) - #1107

Merged
jeremydmiller merged 1 commit into
mainfrom
fix/1101-clone-event-correlation
Nov 2, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
fix/1101-clone-event-correlation

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Follow-up to #1105 for #1101.

#1105 made slice.PublishMessage(message) carry the correlation id that all of a slice's events agree on, and treats a null on an appended event as no agreement. The methods that clone an event dropped CorrelationId and CausationId, so any slice built by fan out or ReplaceEvent held nulls and quietly kept the original bug.

There were two clones, not one

Clone Reached by
CloneEventWithNewData a slicer's fan out rules (FanOutOperator → JasperFx.Events.Grouping.EventListExtensions.FanOut), both ReplaceEvent overloads
WithData EventSlice.FanOut (→ JasperFx.Events.EventListExtensions.FanOut), so SliceGroup.FanOutOnEach, and custom groupers, as in Marten's sample

The two had identical bodies, as do the two FanOut extensions. CloneEventWithNewData now copies both ids, and WithData delegates to it so they cannot drift apart again.

Behaviour change

Nothing a store writes changes. This is from reading Marten 2f2769bca, Polecat 85452490c, Fisher 3f4c4bebd and Weasel at their heads. None of it was run against a store.

Marten Polecat Fisher InMemory
StoreProjection reads from the last event Version, Sequence nothing Version, Sequence nothing
EF Core StoreProjection reads nothing nothing nothing n/a
Document correlation_id / causation_id, ITracked session session session session

No store sets a session's ids from an event, and no store overrides ApplyMetadata. Clones are never persisted either, because BuildOperations writes only RaisedEvents, which are new events.

What does change:

Where Before After
PublishMessage(message) on a fan out or enriched slice whose events agree tenant-only call, so the bus's id for the whole daemon page carries the agreed correlation id, like any other slice
MessageMetadata.From(clone) null correlation the source's correlation
User projection code reading CorrelationId / CausationId off a clone in Apply, Create, ShouldDelete, Evolve or ApplyMetadata null the source's value

The last row can change stored document bodies, but only in projections that copy these ids onto their own documents, which is what Marten documents ApplyMetadata for. Today doc.X = e.CorrelationId in ApplyMetadata ends on null whenever a clone is last in the slice, because the hook runs for every event. Documents already stored keep their value until their next event or a rebuild.

Causation stays off messages: neither slice.PublishMessage nor MessageMetadata.From copies it, and a test pins that for clones. Slices with two flows still fall back to the tenant, since a clone only repeats its source's id and so can never break or create a false agreement.

Not changed

UserName, IsArchived, IsSkipped, AggregateTypeName, EventTypeName, DotNetTypeName and tags are still not copied, so both doc comments still overclaim. UserName would need the same trace, since ApplyMetadata exists for "LastModifiedBy".

Testing

  • ./build.sh Test --root "$PWD": every target succeeded, on net9.0 and net10.0 where the project targets both. EventTests 1735 passed, EventStoreTests 350 passed with 23 skipped.
  • With EventExtensions.cs and EventTypeExtensions.cs back at main, 14 EventTests cases fail, including fan out through both FanOut extensions, ReplaceEvent, the async runner and the inline path in both fan out modes. Two guard cases for slices that don't agree pass either way, as they should.
  • The stores were not rebuilt or run against this change.

🤖 Generated with Claude Code

CloneEventWithNewData and WithData dropped CorrelationId and CausationId, so
a slice built by fan out or ReplaceEvent held nulls and PublishMessage(message)
fell back to the tenant-only call that #1105 fixed for every other slice.

There are two clone methods and two FanOut extensions. A slicer's fan out rules
and ReplaceEvent use CloneEventWithNewData, while EventSlice.FanOut, and so
SliceGroup.FanOutOnEach, uses WithData. WithData now delegates so the two
cannot drift apart again.

No store writes these ids from an event: Marten and Fisher read only Version and
Sequence from the last event, Polecat reads nothing, and the document columns
and ITracked come from the session. A projection that reads them off a clone
itself now sees the source's value instead of null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 82e9f48 into main Nov 2, 2026
1 check passed
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.

1 participant