Skip to content

JasperFx.Events 1.29.0: per-message metadata on side-effect PublishMessage#186

Merged
jeremydmiller merged 1 commit intomainfrom
feature/side-effect-message-metadata
Apr 20, 2026
Merged

JasperFx.Events 1.29.0: per-message metadata on side-effect PublishMessage#186
jeremydmiller merged 1 commit intomainfrom
feature/side-effect-message-metadata

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Adds an independent path through the side-effect publishing pipeline that lets projection authors attach per-message metadata (correlation id, causation id, tenant, headers, user name) when calling PublishMessage from inside RaiseSideEffects.

Motivation

Wolverine's Marten integration builds a fresh MessageContext for side-effect publishing, which means the outgoing Wolverine envelope is never stamped with the originating event's correlation id. Users have no per-message override because PublishMessage only accepts the message object itself. See JasperFx/wolverine#2545 for the motivating scenario.

This PR opens the path. The Marten and Wolverine PRs will follow once this is released.

New surface

  • MessageMetadata struct implementing IMetadataContext — a lightweight value carrier. Headers lazy-allocates so unused metadata stays allocation-free.
  • IMessageSink.PublishAsync<T>(T message, MessageMetadata metadata) — new overload with a default implementation forwarding to the tenant-only overload.
  • IEventSlice<T>.PublishMessage(object message, MessageMetadata metadata) + PublishedMessagesWithMetadata() — same story.
  • IProjectionBatch.PublishMessageAsync(object message, MessageMetadata metadata) — same story.

Internal routing

EventSlice<TDoc,TId> keeps the original PublishedMessages list untouched and adds a separate PublishedMessagesWithMetadata list. The three projection dispatch sites (AggregationRunner, inline single-stream, and inline multi-stream projections) iterate both lists independently, so the original path remains byte-identical for callers that don't opt in.

Compatibility

All new interface members carry default implementations that drop the metadata and forward to the existing signatures. Every existing implementation of IMessageSink / IEventSlice<T> / IProjectionBatch compiles and runs unchanged. Strictly additive.

Test plan

  • dotnet test src/EventTests — 219/219 pass
  • dotnet test src/EventStoreTests — 72/72 pass
  • CI across net8 / net9 / net10

Downstream

Once this ships as JasperFx.Events 1.29.0:

  1. Marten: add an overload on ProjectionBatch.PublishMessageAsync forwarding to IMessageSink.PublishAsync(message, metadata).
  2. Wolverine: implement the metadata-aware overload on MartenToWolverineMessageBatch, mapping to DeliveryOptions (Automatic correlation ID propagation through event-sourced handler chains wolverine#2545).

🤖 Generated with Claude Code

Adds an independent path through the side-effect publishing pipeline that
lets projection authors attach per-message metadata (correlation id,
causation id, tenant, headers, user name) when calling PublishMessage
from inside RaiseSideEffects.

Motivation: Wolverine's Marten integration currently builds a fresh
MessageContext for side-effect publishing, which means the outgoing
Wolverine envelope is never stamped with the originating event's
correlation id. Without a per-message override at the JasperFx level,
there is no user-addressable path to fix this — PublishMessage only
accepts the message object itself. See JasperFx/wolverine#2545 for the
motivating scenario.

## New surface

- MessageMetadata struct implementing IMetadataContext — a lightweight
  value carrier for the metadata. Headers lazy-allocate.
- IMessageSink.PublishAsync<T>(T message, MessageMetadata metadata) —
  new overload with a default implementation forwarding to the
  tenant-only overload for back-compat.
- IEventSlice<T>.PublishMessage(object message, MessageMetadata metadata)
  + PublishedMessagesWithMetadata() — same story.
- IProjectionBatch.PublishMessageAsync(object message, MessageMetadata
  metadata) — same story.

## Internal routing

EventSlice<TDoc,TId> keeps the original PublishedMessages list untouched
and adds a separate PublishedMessagesWithMetadata list. The three
projection dispatch sites (AggregationRunner, inline single-stream, and
inline multi-stream projections) iterate both lists independently, so
the original path remains byte-identical for callers that don't opt in.

## Compatibility

All new interface members carry default implementations that drop the
metadata and forward to existing signatures, so every existing
implementation of IMessageSink / IEventSlice<T> / IProjectionBatch
compiles and runs unchanged. This is strictly additive — no breaking
binary or source changes.

## Tests

EventTests: 219/219 pass.
EventStoreTests: 72/72 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 5fc93b1 into main Apr 20, 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