Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 69 additions & 3 deletions docs/guide/messaging/header-propagation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
# Header Propagation

When consuming messages from external systems, those messages may carry custom headers that need to flow through to any downstream messages your handlers produce. Use `PropagateIncomingHeadersToOutgoing` to declare which headers should be forwarded automatically:
Wolverine can automatically forward headers from an incoming message to all outgoing messages produced within the same handler context. This is useful for propagating correlation identifiers, tracing metadata, or contextual information like "on-behalf-of" across a chain of messages.

## Propagating a Single Header

Use `PropagateIncomingHeaderToOutgoing` when you need to forward just one header:

```csharp
builder.Host.UseWolverine(opts =>
{
// Forward the "on-behalf-of" header to all downstream messages
opts.Policies.PropagateIncomingHeaderToOutgoing("x-on-behalf-of");
});
```

This is ideal for scenarios like middleware that detects delegated user actions and marks outgoing messages to support downstream logging and auditing:

```csharp
// In your middleware, set the header on the incoming envelope:
context.Envelope.Headers["x-on-behalf-of"] = impersonatedUser;

// Any messages published within this handler context will
// automatically carry the "x-on-behalf-of" header
```

## Propagating Multiple Headers

Use `PropagateIncomingHeadersToOutgoing` to forward several headers at once:

```csharp
builder.Host.UseWolverine(opts =>
Expand All @@ -9,6 +35,46 @@ builder.Host.UseWolverine(opts =>
});
```

When a handler receives a message carrying any of the named headers, Wolverine will copy those headers onto every outgoing message cascaded within that handler context. Headers not present on the incoming message are silently skipped.
## Behavior

When a handler receives a message carrying any of the named headers, Wolverine will copy those headers onto every outgoing message cascaded within that handler context. Headers not present on the incoming message are silently skipped — no errors are thrown.

This works across all transports — Kafka, RabbitMQ, Azure Service Bus, or any other.

::: warning
The headers must be present on the incoming `Envelope` at the point the handler runs. Wolverine's default envelope mappers only carry Wolverine's own metadata headers, so if you need to propagate custom headers from an external producer you will need a custom envelope mapper that explicitly reads those headers from the transport message and sets them on the envelope.
:::

## Custom Envelope Rules

For more advanced header manipulation, you can implement a custom `IEnvelopeRule`:

```csharp
public class OnBehalfOfRule : IEnvelopeRule
{
// Called when publishing outside a handler context
public void Modify(Envelope envelope) { }

// Called within a handler context with access to the incoming message
public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
{
var incoming = originator.Envelope;
if (incoming is null) return;

if (incoming.Headers.TryGetValue("x-on-behalf-of", out var value))
{
outgoing.Headers["x-on-behalf-of"] = value;
outgoing.Headers["x-audit-trail"] = $"delegated:{value}";
}
}
}
```

Register your custom rule as a metadata rule:

This works across all transports — Kafka, RabbitMQ, Azure Service Bus, or any other. The headers must be present on the incoming `Envelope` at the point the handler runs. Wolverine's default envelope mappers only carry Wolverine's own metadata headers, so if you need to propagate custom headers from an external producer you will need a custom envelope mapper that explicitly reads those headers from the transport message and sets them on the envelope.
```csharp
builder.Host.UseWolverine(opts =>
{
opts.MetadataRules.Add(new OnBehalfOfRule());
});
```
4 changes: 4 additions & 0 deletions docs/guide/messaging/message-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,10 @@ public static async Task SendMessagesWithDeliveryOptions(IMessageBus bus)
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/DocumentationSamples/CustomizingMessageDelivery.cs#L9-L27' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_sendmessageswithdeliveryoptions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: tip
If you need to automatically forward headers from incoming messages to all outgoing messages within a handler context, see [Header Propagation](/guide/messaging/header-propagation).
:::

## Sending Raw Message Data <Badge type="tip" text="5.8" />

In some particular cases, you may want to use Wolverine to send a message to another system (or the same system)
Expand Down
63 changes: 63 additions & 0 deletions src/Testing/CoreTests/PropagateHeadersRuleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,66 @@ public void no_op_when_there_is_no_incoming_envelope()
outgoing.Headers.ContainsKey("x-custom").ShouldBeFalse();
}
}

public class PropagateOneHeaderRuleTests
{
[Fact]
public void modify_is_a_no_op_outside_of_a_handler_context()
{
var rule = new PropagateOneHeaderRule("x-on-behalf-of");
var envelope = ObjectMother.Envelope();

rule.Modify(envelope);

envelope.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
}

[Fact]
public void copies_single_header_from_incoming_to_outgoing()
{
var rule = new PropagateOneHeaderRule("x-on-behalf-of");

var incoming = ObjectMother.Envelope();
incoming.Headers["x-on-behalf-of"] = "admin-user";
incoming.Headers["x-other"] = "should-not-propagate";

var context = Substitute.For<IMessageContext>();
context.Envelope.Returns(incoming);

var outgoing = ObjectMother.Envelope();
rule.ApplyCorrelation(context, outgoing);

outgoing.Headers["x-on-behalf-of"].ShouldBe("admin-user");
outgoing.Headers.ContainsKey("x-other").ShouldBeFalse();
}

[Fact]
public void header_not_present_on_incoming_is_silently_skipped()
{
var rule = new PropagateOneHeaderRule("x-on-behalf-of");

var incoming = ObjectMother.Envelope();

var context = Substitute.For<IMessageContext>();
context.Envelope.Returns(incoming);

var outgoing = ObjectMother.Envelope();
rule.ApplyCorrelation(context, outgoing);

outgoing.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
}

[Fact]
public void no_op_when_there_is_no_incoming_envelope()
{
var rule = new PropagateOneHeaderRule("x-on-behalf-of");

var context = Substitute.For<IMessageContext>();
context.Envelope.Returns((Envelope?)null);

var outgoing = ObjectMother.Envelope();
rule.ApplyCorrelation(context, outgoing);

outgoing.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
}
}
28 changes: 28 additions & 0 deletions src/Wolverine/IEnvelopeRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,34 @@ public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
}
}

/// <summary>
/// Propagates a single named header from the incoming envelope to the outgoing envelope
/// if it exists. This is a convenience rule for the common case of forwarding just one header.
/// </summary>
internal class PropagateOneHeaderRule : IEnvelopeRule
{
private readonly string _headerName;

public PropagateOneHeaderRule(string headerName)
{
_headerName = headerName;
}

// No incoming context available outside a handler — nothing to propagate
public void Modify(Envelope envelope) { }

public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
{
var incoming = originator.Envelope;
if (incoming is null) return;

if (incoming.Headers.TryGetValue(_headerName, out var value))
{
outgoing.Headers[_headerName] = value;
}
}
}

internal class LambdaEnvelopeRule : IEnvelopeRule
{
private readonly Action<Envelope> _configure;
Expand Down
7 changes: 7 additions & 0 deletions src/Wolverine/IPolicies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,11 @@ public interface IPolicies : IEnumerable<IWolverinePolicy>, IWithFailurePolicies
/// message are silently skipped.
/// </summary>
void PropagateIncomingHeadersToOutgoing(params string[] headerNames);

/// <summary>
/// Automatically propagate a single named header from an incoming message to all outgoing
/// messages cascaded within the same handler context. If the header is not present on the
/// incoming message it is silently skipped.
/// </summary>
void PropagateIncomingHeaderToOutgoing(string headerName);
}
8 changes: 8 additions & 0 deletions src/Wolverine/WolverineOptions.Policies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@ void IPolicies.PropagateIncomingHeadersToOutgoing(params string[] headerNames)
MetadataRules.Add(new PropagateHeadersRule(headerNames));
}

void IPolicies.PropagateIncomingHeaderToOutgoing(string headerName)
{
if (string.IsNullOrWhiteSpace(headerName))
throw new ArgumentException("A header name is required", nameof(headerName));

MetadataRules.Add(new PropagateOneHeaderRule(headerName));
}

internal MiddlewarePolicy FindOrCreateMiddlewarePolicy()
{
var policy = RegisteredPolicies.OfType<MiddlewarePolicy>().FirstOrDefault();
Expand Down
Loading