If you have worked in .NET long enough, you have probably wired up MediatR, registered your pipeline behaviours, and moved on. It works. But at some point you need durable messaging, an outbox, scheduled retries, or external transport — and suddenly the lightweight mediator that got you started is nowhere near enough. You bolt on MassTransit or NServiceBus beside it, maintain two sets of conventions, and wonder why your handlers feel like they serve two masters.
Wolverine takes a different approach. Built by Jeremy D. Miller (the mind behind Lamar, Marten, and the original Jasper project), Wolverine is a single library that handles in-process mediation, asynchronous messaging, durable outbox delivery, and HTTP endpoint generation — all through the same handler model. It has quietly grown to over 3.7 million NuGet downloads, ships regular releases (v5.27 landed at the end of March 2026), and targets .NET 8, 9, and 10. If you have not looked at it yet, now is a good time.
What Wolverine actually is
At its core, Wolverine is a message bus. You define messages (plain C# classes or records) and handlers (plain methods), and Wolverine routes one to the other. But unlike MediatR, it does not stop at in-process dispatch. The same handler model extends to RabbitMQ, Azure Service Bus, Amazon SQS, and Kafka transports. A handler you write for local invocation today can be moved to a durable queue tomorrow with a configuration change, not a code change.
The framework integrates into the standard IHost pipeline:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
// Wolverine will scan the application assembly for handlers automatically
});
var app = builder.Build();
app.Run();
That single UseWolverine() call registers the message bus, discovers handlers by convention, and sets up the internal execution pipeline. There are no marker interfaces to implement, no explicit handler registrations to maintain.
Handlers without the ceremony
This is where Wolverine diverges most from MediatR. There are no required interfaces on message types or handler types. You write a public class ending in Handler or Consumer, give it a public method called Handle or Consume, and you are done.
public static class CreateOrderHandler
{
public static OrderCreated Handle(
CreateOrder command,
IDocumentSession session)
{
var order = new Order
{
CustomerId = command.CustomerId,
Items = command.Items
};
session.Store(order);
return new OrderCreated(order.Id);
}
}
Several things are happening here that are worth unpacking:
- No interfaces. The handler is a plain static class. No
IRequestHandler<TRequest, TResponse>, noINotificationHandler<T>. - Method injection. The
IDocumentSessionparameter is resolved from the DI container at invocation time — not through constructor injection, though that is supported too. - Cascading messages. The
OrderCreatedreturn value is automatically published as a new message after the handler completes. If nothing should be published, returnvoidorTask. - Static handlers. Because the class is static, Wolverine avoids allocating a handler instance per message — a small but meaningful win at scale.
For comparison, the equivalent MediatR handler requires a class that implements IRequestHandler<CreateOrder, OrderCreated>, a constructor accepting dependencies, and a Handle method with a CancellationToken parameter you probably ignore.
Calling handlers from ASP.NET Core
The IMessageBus service is registered automatically and can be injected into your endpoints:
app.MapPost("/orders", async (CreateOrder command, IMessageBus bus) =>
{
var created = await bus.InvokeAsync<OrderCreated>(command);
return Results.Created($"/orders/{created.OrderId}", created);
});
InvokeAsync<T> processes the command synchronously (within the current request) and returns the cascading message. If you do not need the response, InvokeAsync without a type parameter fires the handler and discards any return value.
For fire-and-forget scenarios where you want the message queued locally:
await bus.PublishAsync(new SendOrderConfirmationEmail(order.Id));
This enqueues the message to a local, in-memory queue by default. With durable inbox configuration, it survives process restarts.
Middleware that generates code, not allocations
MediatR pipeline behaviours use a nested delegate pattern. Each behaviour wraps the next, and the runtime walks the chain on every request. It works, but it allocates closures, and every behaviour runs even when it has nothing to do for a particular message type.
Wolverine takes a fundamentally different approach. At startup, it inspects your handlers and middleware, then generates C# source code that weaves them together into a single, flat execution method. No reflection at runtime, no delegate chains, no unnecessary allocations.
Middleware classes follow the same naming conventions as handlers:
public class FluentValidationMiddleware<T>
{
public static async Task<HandlerContinuation> BeforeAsync(
T message,
IValidator<T> validator)
{
var result = await validator.ValidateAsync(message);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
return HandlerContinuation.Continue;
}
}
Methods named Before, BeforeAsync, Load, or Validate run before the handler. Methods named After, PostProcess, or Finally run after. Wolverine generates the wiring code at configuration time, so the runtime cost is a direct method call — not a chain of delegates.
The key advantage: Wolverine inspects the DI container and only applies middleware when its dependencies are actually registered. If you register a FluentValidation validator for CreateOrder but not for CancelOrder, the generated code for CancelOrder will not include the validation step at all.
Apply middleware globally through policies:
builder.Host.UseWolverine(opts =>
{
opts.Policies.AddMiddleware<FluentValidationMiddleware<object>>();
});
Or target specific message types:
opts.Policies.ForMessagesOfType<IAccountCommand>()
.AddMiddleware(typeof(AccountLookupMiddleware));
The durable outbox: messaging that survives crashes
This is where Wolverine leaves mediator-only libraries behind entirely. The transactional outbox pattern ensures that database writes and message publishing either both succeed or both fail. Without it, you risk scenarios where a database transaction commits but the message to RabbitMQ is lost — or vice versa.
Wolverine has built-in outbox support for both Entity Framework Core and Marten (PostgreSQL):
builder.Host.UseWolverine(opts =>
{
opts.UseEntityFrameworkCoreTransactions();
opts.PersistMessagesWithSqlServer(connectionString);
opts.Policies.UseDurableLocalQueues();
});
With this configuration, messages published during a handler are stored in the same database transaction as your business data. A background agent polls for unsent messages and delivers them to their transport. If the process crashes between the commit and delivery, messages are picked up on restart.
The [Transactional] attribute on a handler method tells Wolverine to wrap the entire handler in a database transaction and use the outbox automatically:
public static class PlaceOrderHandler
{
[Transactional]
public static OrderPlaced Handle(
PlaceOrder command,
OrderDbContext db)
{
var order = new Order { CustomerId = command.CustomerId };
db.Orders.Add(order);
// OrderPlaced is published via the outbox — guaranteed delivery
return new OrderPlaced(order.Id);
}
}
No manual SaveChangesAsync call. No explicit IMessageBus.PublishAsync. Wolverine's generated code handles the transaction commit and outbox storage together.
Compound handlers: separating data loading from logic
A common complaint about handler patterns is that they mix data access with business logic. Wolverine addresses this with compound handlers — multiple methods on the same class that form a pipeline:
public static class ShipOrderHandler
{
public static async Task<(Order, Customer)> LoadAsync(
ShipOrder command,
IDocumentSession session)
{
var order = await session.LoadAsync<Order>(command.OrderId);
var customer = await session.LoadAsync<Customer>(order!.CustomerId);
return (order, customer!);
}
public static IEnumerable<object> Handle(
ShipOrder command,
Order order,
Customer customer)
{
order.Ship(customer.ShippingAddress);
yield return new OrderShipped(order.Id);
yield return new NotifyCustomer(customer.Email, order.Id);
}
}
The LoadAsync method fetches the data. Wolverine feeds its return values into the Handle method as parameters. The Handle method contains pure business logic with no I/O — trivially testable without mocking a database session.
Error handling and retries
Wolverine's error handling is built into the handler pipeline, not bolted on through Polly wrappers or custom pipeline behaviours:
builder.Host.UseWolverine(opts =>
{
opts.OnException<SqlException>()
.RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds());
opts.OnException<TimeoutException>()
.Requeue(3);
opts.OnException<InvalidOperationException>()
.MoveToErrorQueue();
});
Policies apply globally or per-handler. The retry logic runs inside the generated code, so there is no additional middleware overhead for messages that succeed on the first attempt.
When to choose Wolverine
Wolverine is not the right tool for every situation. If you need a lightweight mediator with zero learning curve and no messaging ambitions, MediatR still does that job well. But consider Wolverine when:
- You need both in-process mediation and asynchronous messaging. Wolverine handles both with the same handler model, eliminating the need for separate libraries.
- Durable message delivery matters. The built-in outbox support is production-grade and works with SQL Server, PostgreSQL (via Marten), and the local file system.
- Performance is a concern. The code generation approach eliminates the per-message allocations and reflection costs that accumulate in high-throughput systems.
- You are already using Marten. The Wolverine + Marten combination (the "Critter Stack") is particularly powerful for event sourcing and CQRS, with deep integration between the two.
// TIP
If you are evaluating Wolverine as a MediatR replacement, start with DurabilityMode.MediatorOnly — it disables all asynchronous messaging and keeps the footprint minimal while you migrate handlers.
Common pitfalls
Forgetting naming conventions. Wolverine discovers handlers by convention. If your class does not end in Handler or Consumer, or your method is not named Handle or Consume, it will be silently ignored. The [WolverineHandler] attribute can mark classes explicitly if conventions do not suit your naming style.
Mixing constructor injection with static handlers. Static handlers cannot use constructor injection. If you mark a handler static for performance but then need a scoped service, switch to method injection (add the dependency as a method parameter) rather than making the class non-static.
Publishing without durability. By default, PublishAsync sends to an in-memory queue. If the process restarts, those messages are lost. For anything that matters, configure UseDurableLocalQueues() or an external transport.
Cascading messages you did not intend. Every non-void return value from a handler is treated as a cascading message. If your handler returns a DTO for convenience, Wolverine will try to route it. Return void or Task when you do not want cascading behaviour.
Not inspecting generated code. Wolverine can write its generated handler code to disk with opts.CodeGeneration.TypeLoadMode = TypeLoadMode.GenerateCodeDynamically. Reviewing this output helps you understand exactly what the middleware pipeline looks like for each handler — invaluable when debugging unexpected behaviour.
Summary
- Wolverine is a message bus that doubles as an in-process mediator, removing the need to run MediatR alongside a separate messaging library.
- Handlers are plain methods discovered by convention — no marker interfaces, no base classes, no explicit registration.
- The middleware pipeline is code-generated at startup, eliminating runtime reflection and reducing allocations compared to delegate-chain approaches.
- The transactional outbox ensures database writes and message publishing are atomic, with built-in support for EF Core and Marten.
- Compound handlers separate data loading from business logic, making handlers easier to test.
- Error handling, retries, and dead-letter routing are first-class features configured through a fluent API.
- At 3.7 million downloads and regular releases, Wolverine is production-ready and actively maintained — worth evaluating if your project has outgrown a mediator-only model.