Skip to content

Per-message metadata override for projection side-effect publishing (GH-2545)#2550

Merged
jeremydmiller merged 1 commit intomainfrom
feature/2545-investigate
Apr 21, 2026
Merged

Per-message metadata override for projection side-effect publishing (GH-2545)#2550
jeremydmiller merged 1 commit intomainfrom
feature/2545-investigate

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Closes #2545.

Lets a Marten projection's RaiseSideEffects pass per-message metadata (correlation id, causation id, tenant, headers) when it calls slice.PublishMessage(...), and have those values flow all the way through to the receiving handler's IMessageContext and Marten IDocumentSession. This is the first-class way to thread a business correlation id through an event → side-effect command → derived event chain — the "todo-list" pattern from the issue.

Cross-repo recap

This PR is the final step in a four-PR chain:

  1. JasperFx.Events 1.29.0: per-message metadata on side-effect PublishMessage jasperfx#186JasperFx.Events 1.29.0: MessageMetadata struct + metadata overloads on IMessageSink, IEventSlice<T>, IProjectionBatch.
  2. Marten batch: JasperFx.Events 1.29.0 + GH-4267 + GH-4268 reproducer attempt marten#4269 — Marten 8.32.0: ProjectionBatch.PublishMessageAsync(object, MessageMetadata) overload.
  3. Weasel 8.14.1: upgrade to JasperFx 1.26 + fix Spectre.Console 0.55 API break weasel#252 — Weasel 8.14.1: companion fix for Spectre.Console 0.55 API break.
  4. This PR: Wolverine consumes the overload, maps it to DeliveryOptions, and stamps the receiving handler.

Plumbing

  • Bumps in Directory.Packages.props: JasperFx.Events 1.28.1 → 1.29.0, Marten / Marten.AspNetCore 8.31 → 8.32, Weasel.* 8.14.0 → 8.14.1.
  • DeliveryOptions: new CorrelationId and CausationId properties. Override(envelope) sets envelope.CorrelationId directly and stores CausationId in envelope.Headers["causation-id"] (string semantics preserve user-supplied values that aren't Guids — Wolverine's native ConversationId is Guid-typed and would lose information).
  • MessageBus.TrackEnvelopeCorrelation: no longer clobbers a pre-existing envelope.CorrelationId. This was the load-bearing reason a DeliveryOptions.CorrelationId override didn't survive routing — the late-running correlation tracker was overwriting it with the (null) MessageContext correlation id.
  • MartenToWolverineMessageBatch: implements the new IMessageSink.PublishAsync<T>(T, MessageMetadata) overload by mapping IMetadataContext fields onto a DeliveryOptions and delegating to Context.PublishAsync.
  • OutboxedSessionFactory.configureSession: when an envelope carries the new "causation-id" header (set via DeliveryOptions.CausationId), use it as session.CausationId in preference to the default ConversationId.ToString() chain. This is the receiving-side hook that closes the loop — events written by the handler inherit the user's chosen causation id.

Test

MartenTests/Bugs/Bug_2545_raise_side_effects_with_metadata_override.cs runs an async-daemon projection that publishes a side-effect command with explicit MessageMetadata, and asserts the handler observes:

  1. IMessageContext.CorrelationId matches the override
  2. IDocumentSession.CorrelationId matches the override
  3. IDocumentSession.CausationId matches the override

Docs

New "Overriding Side-Effect Message Metadata" section appended to docs/guide/durability/marten/event-forwarding.md, covering the motivating todo-list use case and a field-by-field mapping table (MessageMetadata → envelope → handler/session).

Test plan

  • dotnet test src/Testing/CoreTests/CoreTests.csproj --framework net9.01346/1346 pass
  • dotnet test src/Persistence/MartenTests/MartenTests.csproj --framework net9.0 --filter "publish_messages|Bug_2545|MartenOutbox|AncillaryStores"23/23 pass
  • Bug_2545 integration test — pass
  • Full CI matrix

🤖 Generated with Claude Code

…H-2545)

Lets a Marten projection's RaiseSideEffects pass per-message metadata
(correlation id, causation id, tenant, headers) when it publishes a
Wolverine command, and have those values flow all the way through to
the receiving handler's IMessageContext and Marten IDocumentSession.
This is the first-class way to thread a business correlation id
through an event → side-effect command → derived event chain (the
"todo-list" pattern from the issue).

## Plumbing

- Bump JasperFx.Events 1.28.1 -> 1.29.0, Marten 8.31 -> 8.32,
  Marten.AspNetCore 8.31 -> 8.32, and Weasel.* 8.14.0 -> 8.14.1
  in Directory.Packages.props to pick up the new IMessageSink /
  IEventSlice / IProjectionBatch metadata overloads (#186,
  marten#4269) and the Spectre.Console 0.55 fix (weasel#252).

- DeliveryOptions: add CorrelationId and CausationId properties.
  Override(envelope) sets envelope.CorrelationId directly and stores
  CausationId in envelope.Headers["causation-id"] (string semantics
  preserve user-supplied values that aren't Guids — Wolverine's
  native ConversationId is Guid-typed and would lose information).

- MessageBus.TrackEnvelopeCorrelation no longer clobbers a
  pre-existing envelope.CorrelationId. This was the load-bearing
  reason a DeliveryOptions.CorrelationId override didn't survive
  routing — the late-running correlation tracker was overwriting
  it with the (null) MessageContext correlation id.

- MartenToWolverineMessageBatch: implement the new
  IMessageSink.PublishAsync<T>(T, MessageMetadata) overload by
  mapping IMetadataContext fields onto a DeliveryOptions and
  delegating to Context.PublishAsync.

- OutboxedSessionFactory.configureSession: when an envelope carries
  the new "causation-id" header (set via DeliveryOptions.CausationId),
  use it as session.CausationId in preference to the default
  ConversationId.ToString() chain. This is the receiving-side hook
  that closes the loop on the metadata override — events written by
  the handler inherit the user's chosen causation id.

## Test

MartenTests/Bugs/Bug_2545_raise_side_effects_with_metadata_override.cs
runs an async-daemon projection that publishes a side-effect command
with explicit MessageMetadata, and asserts the handler observes the
overridden CorrelationId on its MessageContext and both the
overridden CorrelationId AND CausationId on its Marten session.

Local verification:
- Bug_2545 integration test: 1/1 pass
- MartenTests subset (publish/outbox/ancillary): 23/23 pass
- Wolverine CoreTests: 1346/1346 pass

## Docs

New "Overriding Side-Effect Message Metadata" section appended to
docs/guide/durability/marten/event-forwarding.md, covering the
motivating "todo-list" use case and the field-by-field mapping table
(MessageMetadata -> envelope -> handler/session).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Automatic correlation ID propagation through event-sourced handler chains

1 participant