Messaging Middleware - Handling Cross‑Cutting Concerns with NServiceBus Behaviors

September 23, 2025#Software Development
Series: NServiceBus
Article
Author image.

Kyle McMaster, Architect

This post is part of a series on NServiceBus. In the previous articles, we looked at fundamental concepts for messaging and how to get started with NServiceBus. All the code for this series can be found in the NServiceBusTutorial repository on GitHub.

Introduction to the NServiceBus Pipeline 🛤️

The NServiceBus pipeline is a configurable sequence of steps (forks, behaviors, mutators, etc.) that every message passes through on the way in 📩 (transport -> serialization -> logical message -> handlers) and on the way out 📤 (handler -> routing -> serialization -> dispatch). Each stage is composed of built‑in behaviors handling low-level messaging and core concerns like transport transaction and recoverability, message mutation, routing, serialization, and physical dispatch. The magic of the pipeline comes from registering custom behaviors that slot into the physical (before deserialization) or logical (after deserialization) stages, giving you control to short‑circuit, wrap execution for timing or telemetry, modify headers, or enforce governance rules globally. This post zooms in on creating custom behaviors, how they work, when to use them, and how to keep them lean and testable.

What Are Behaviors? 🤔

I often think of behaviors as the equivalent of ASP.NET Core middleware for the NServiceBus message pipeline. Every incoming or outgoing message passes through a configurable pipeline of steps, behaviors, and mutators. A behavior lets you inspect, enrich, validate, measure, or enforce policies—without touching every handler. This allows for clean handlers and centralized cross‑cutting concerns. They’re awesome because they enable consistent correlation IDs, performance timing, OpenTelemetry enrichment, and can even repair malformed messages. Behaviors enable functionality without leaking infrastructure concerns into business code in your handlers.

A Practical Example 🧪

The following code sample shows a simple correlation behavior that ensures every message has a CorrelationId header and adds it to the logging context for better traceability.

public class CorrelationIdBehavior :
  Behavior<IIncomingLogicalMessageContext, IIncomingLogicalMessageContext>
{
  public override async Task Invoke(
    IIncomingLogicalMessageContext context,
    Func<IIncomingLogicalMessageContext, Task> next)
  {
    if (!context.Headers.TryGetValue("CorrelationId", out var correlationId))
    {
      correlationId = Guid.NewGuid().ToString();
      context.Headers["CorrelationId"] = correlationId;
    }

    using (YourLogger.Context.PushProperty("CorrelationId", correlationId))
    {
      await next(context);
    }
  }
}

// Register the behavior with your endpoint configuration
endpointConfiguration.Pipeline.Register(
    behavior: new CorrelationIdBehavior(),
    description: "Adds or propagates a CorrelationId header and enriches logging context.");

Unit Testing

Use the NServiceBus testing framework to create unit tests for your behaviors. This allows you to simulate the pipeline and assert that your behavior modifies the context as expected, calls the next step, or short-circuits processing. The following is a short example test for the CorrelationIdBehavior:

[Fact]
public async void ShouldAddCorrelationIdIfMissing()
{
  var behavior = new CorrelationIdBehavior();
  var context = new TestableIncomingLogicalMessageContext();

  await behavior.Invoke(context, ctx => Task.CompletedTask);

  Assert.IsTrue(context.Headers.ContainsKey("CorrelationId"));
}

Practical Tips ✅

One should always try to keep each behavior focused and concise - be sure to follow the single responsibility principle. It’s important to make them idempotent so retries don’t create duplicate side effects. When passing state between behaviors, prefer headers or context accessors while avoiding static or ambient state. Remember that registration order matters. Behaviors run in the order they are added, so place critical ones early in the pipeline. While most behaviors will likely have minimal overhead, if performance is a major concern for your domain consider measuring execution time as needed.

Wrap Up 🎁

NServiceBus behaviors give you middleware superpowers for messaging pipelines. This means clean handlers, consistent policies, richer observability, and safer evolvability. Any time you think “I need this around every message,” a behavior is a strong candidate. Be sure to keep them sharp, fast, and focused—and your pipeline becomes a powerful, composable asset in your NServiceBus architecture. Happy messaging! 💌

Resources 📚


Copyright © 2025 NimblePros - All Rights Reserved