Migrating from MediatR to Wolverine in .NET: A Step-by-Step Guide

If your team is planning to migrate MediatR to Wolverine, the good news is that the mechanical part is far smaller than it looks. MediatR handlers are already single-responsibility classes with one method; Wolverine wants the same thing with less ceremony. The hard part is not the handlers. It is the pipeline behaviours, the assembly scanning, and deciding whether to do the move in one commit or gradually behind shim interfaces.
I've run this migration on a codebase with roughly ninety handlers, and the honest summary is: two days of mostly-mechanical work, one genuinely tricky afternoon on the validation behaviour, and a meaningful reduction in boilerplate at the end. This guide is the path I would follow again, in order, including the pitfalls that cost me the most time. If you want the before-and-after codebase with both wirings side by side rather than isolated snippets, the annotated version lives on Patreon.
One thing worth saying up front: this migration is much easier if the MediatR setup it replaces was clean to begin with - commands and queries properly separated, one handler each, behaviours doing cross-cutting work rather than business logic. That structure is exactly what Chapter 11 of the Zero to Production course builds, with LoggingBehavior and ValidationBehavior wired into a four-layer solution, so the pieces you are about to move are already in the right shape.
Targets .NET 10 and current stable Wolverine.
Why Migrate at All?
Be clear-eyed about the reason, because it changes how much effort is justified.
Licensing. MediatR 13.0 and later ship under a dual licence: the Reciprocal Public License 1.5 for open-source use, or a paid commercial licence, under Lucky Penny Software. Jimmy Bogard was explicit that sustainability drove the change, and the community tier means plenty of teams can keep using MediatR at no cost. But RPL-1.5 is a strong copyleft licence, and for a lot of enterprise legal teams "strong copyleft in a shipped product" is a conversation nobody wants to have. That single fact is what put this migration on most roadmaps.
Scope. Wolverine is MIT-licensed and does two jobs where MediatR does one: in-process request and response mediation, plus durable messaging over RabbitMQ, Azure Service Bus, Amazon SQS or Kafka. If your architecture already has a mediator and a separate message bus, consolidating is a real simplification.
Boilerplate. Wolverine discovers handlers by convention. No marker interfaces, no generic constraints, no registration calls.
Reasons not to migrate: you are on a MediatR version you are happy with and your licence position is settled; or your team has deep muscle memory around IPipelineBehavior and no appetite for Wolverine's code-generation model. Staying put is a legitimate choice. Our MediatR vs Wolverine vs Brighter comparison covers that decision properly; this article assumes you have already made it.
What Actually Changes
| MediatR concept | Wolverine equivalent |
|---|---|
IRequest<TResponse> marker on a request |
Nothing. A plain class or record. |
IRequestHandler<TRequest, TResponse> |
A public class with a Handle method. No interface. |
IMediator / ISender injected into a controller |
IMessageBus |
mediator.Send(request) |
bus.InvokeAsync<TResult>(request) |
mediator.Publish(notification) |
bus.PublishAsync(notification) |
IPipelineBehavior<TRequest, TResponse> |
A conventional middleware class, code-generated per handler chain |
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(...)) |
builder.Host.UseWolverine(opts => ...) |
| Handler dependencies via constructor | Constructor or method parameters |
The last row is the one that changes how the code reads. Wolverine can inject dependencies directly into the Handle method, which means many handlers collapse to a single static method with no constructor and no fields at all.
Step 1: Install and Register Wolverine
Add the Wolverine package and register it on the host builder. Wolverine plugs into the generic host, so it goes on builder.Host, not on builder.Services:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine(opts =>
{
// messaging transports, policies and discovery are configured here
});
var app = builder.Build();
Leave the MediatR registration in place for now. Both can coexist, which is what makes a gradual migration possible.
Step 2: Decide Big Bang or Gradual
Wolverine ships shim interfaces in the core package under the Wolverine.Shims.MediatR namespace, providing IRequest, IRequest<T>, IRequestHandler<TRequest> and IRequestHandler<TRequest, TResponse>. Swapping the using MediatR; line for using Wolverine.Shims.MediatR; lets existing handler signatures keep compiling while Wolverine takes over execution.
Use the shims when: you have more than about thirty handlers, or the migration has to land incrementally across several sprints without a long-lived branch.
Skip the shims when: the handler count is small enough to convert in one sitting. The shims are a bridge, not a destination - leaving them in permanently means you keep MediatR's ceremony without MediatR.
Either way, the end state should be conventional Wolverine handlers.
Step 3: Convert the Handlers
This is the bulk of the work and it is genuinely mechanical. A typical MediatR handler:
public sealed class SubmitOrderHandler(IOrderRepository repo)
: IRequestHandler<SubmitOrder, OrderResult>
{
public async Task<OrderResult> Handle(SubmitOrder request, CancellationToken ct)
=> await repo.SubmitAsync(request, ct);
}
becomes:
public static class SubmitOrderHandler
{
public static Task<OrderResult> Handle(
SubmitOrder command, IOrderRepository repo, CancellationToken ct)
=> repo.SubmitAsync(command, ct);
}
The interface is gone, the constructor is gone, the dependency moved into the method signature, and the class became static. Wolverine finds it by scanning for a public class with a Handle method whose first parameter is the message type.
Do not convert to static methods reflexively. A handler with four dependencies and three private helper methods reads better as an instance class with a constructor, and Wolverine supports that unchanged. Use the static form where it genuinely removes noise.
Step 4: Replace the Call Sites
Every IMediator or ISender injection becomes IMessageBus:
app.MapPost("/orders", async (SubmitOrder command, IMessageBus bus) =>
{
var result = await bus.InvokeAsync<OrderResult>(command);
return Results.Ok(result);
});
InvokeAsync<T> is the direct equivalent of Send, with one bonus: because it runs through Wolverine's execution pipeline, error-handling policies such as selective retries apply to it. PublishAsync replaces Publish for notifications.
A find-and-replace gets you ninety percent of the way. The remaining ten percent are places where someone injected IMediator into a domain service, which is a design smell worth fixing while you are in there anyway.
Step 5: Rewrite the Pipeline Behaviours
This is the step that takes real thought, because the model is genuinely different. MediatR behaviours are generic classes that wrap the whole pipeline and run for every request. Wolverine middleware is a conventional class with Before and After methods, woven into each handler chain at code-generation time:
public static class LoggingMiddleware
{
public static void Before(ILogger logger, Envelope envelope)
=> logger.LogInformation("Handling {MessageType}", envelope.Message?.GetType().Name);
public static void After(ILogger logger, Envelope envelope)
=> logger.LogInformation("Handled {MessageType}", envelope.Message?.GetType().Name);
}
Register it with a filter so it applies where you want:
opts.Policies.AddMiddleware<LoggingMiddleware>(chain => /* filter by message type */);
Two consequences worth internalising. First, middleware is filterable per message type, so the "run for everything, then check if it applies" pattern common in MediatR behaviours disappears. Second, because it is code-generated into each chain rather than composed at runtime, there is no next() delegate to await around, and there is no per-request allocation for the wrapper.
Validation is the behaviour that usually causes the tricky afternoon. A MediatR ValidationBehavior typically resolves every IValidator<TRequest>, runs them, and throws. In Wolverine the idiomatic approach is a Before method that returns a result short-circuiting the handler. Budget time for this one; do not leave it to the last hour.
Step 6: Handler Discovery Across Assemblies
MediatR needed RegisterServicesFromAssembly for every assembly containing handlers. Wolverine scans the entry assembly by default. In a Clean Architecture solution where handlers live in an Application project, you must add that assembly through Wolverine's discovery options - otherwise the app starts perfectly and every InvokeAsync fails at runtime with "no handler found."
If you are running the two libraries side by side during a gradual migration, message types shared across the boundary need opts.Policies.RegisterInteropMessageAssembly(assembly) so both stacks agree on identity.
Common Migration Pitfalls
Forgetting the handler assembly. By far the most common failure. Symptom: clean startup, runtime failure on first dispatch. Check discovery configuration first.
Assuming middleware ordering matches behaviour ordering. MediatR behaviours execute in registration order. Wolverine composes chains differently, and if your logging behaviour depended on running strictly outside your validation behaviour, verify that assumption explicitly rather than trusting it carried over.
Leaving the shims in permanently. They compile, tests pass, and the migration quietly stalls at 80 percent. Set a date to remove them.
Not accounting for code generation at startup. Wolverine generates handler code, which has a cost the first time. For most APIs it is unremarkable, but if you have strict cold-start requirements or a Native AOT target, look at Wolverine's ahead-of-time code-generation workflow before you commit rather than after.
Migrating and refactoring in the same commit. Resist it. Convert the handler as-is, get the test green, then improve it separately. Mixed commits make bisecting a regression miserable.
Verification Checklist
Before you delete the MediatR package reference:
[ ] Every handler resolves. A startup test that dispatches one message per registered type catches missing discovery immediately.
[ ] No
using MediatR;orWolverine.Shims.MediatRremains outside a deliberate compatibility layer.[ ] Every former
IPipelineBehaviorhas a Wolverine equivalent, and you have a test proving it runs.[ ] Validation failures produce the same HTTP status and Problem Details shape as before. This is the most user-visible regression risk in the whole migration.
[ ] Integration tests pass against the real host, not just unit tests against handlers.
[ ] Startup time measured before and after, on a container that matches production.
[ ] The MediatR package is actually removed from every
.csproj, including test projects.
How Long Does Migrating from MediatR to Wolverine Take?
For a codebase of fifty to a hundred handlers with a handful of behaviours, plan on two to three focused days: half a day for registration and discovery, one to one and a half days converting handlers and call sites, half a day on behaviours, and half a day on verification. Handler conversion scales roughly linearly and is easily parallelised across a team. The behaviours do not scale linearly - they are a fixed, front-loaded cost regardless of handler count.
The estimate that blows up is a codebase where IMediator leaked into domain services and background jobs. Grep for the injection points before you estimate, not after.
Frequently Asked Questions
Do I Have to Remove MediatR Before Wolverine Will Work?
No. Both register independently and can run in the same process, which is exactly what makes an incremental migration feasible. Keep MediatR registered until the last handler is converted, then remove the package reference and confirm the solution still builds.
What Is the Wolverine Equivalent of ISender.Send?
IMessageBus.InvokeAsync<TResponse>(message). Inject IMessageBus where you previously injected ISender or IMediator. For notifications, IMediator.Publish maps to IMessageBus.PublishAsync.
Does Wolverine Work With Clean Architecture and CQRS?
Yes, and the layering does not change. Commands and queries stay in the Application layer, handlers stay beside them, and the API layer depends inward exactly as before. The only structural difference is that handlers no longer implement a MediatR interface, which slightly reduces the Application layer's external dependencies. See our guide on Clean Architecture with CQRS in ASP.NET Core for the surrounding structure.
Will Migrating Break My Existing Unit Tests?
Handler unit tests mostly survive, because a handler is still a class with a method you can call directly - often more easily, since a static Handle needs no constructor wiring. Tests that mocked IMediator need updating to IMessageBus. Tests asserting behaviour ordering will need rewriting against the middleware model.
Is Wolverine's Code Generation a Problem in Production?
For typical ASP.NET Core APIs, no. It costs some startup time on first use and is otherwise invisible. It matters if you have aggressive cold-start targets, run in an environment where writing generated assemblies is restricted, or are targeting Native AOT. In those cases use the ahead-of-time generation workflow so the code is produced at build time. Evaluate this before committing to the migration.
Where Is the Official Migration Documentation?
The Wolverine migration guide is the authoritative reference for the shim interfaces, discovery configuration and middleware model, and is kept current with each release.
About the Author
I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.
GitHub: codingdroplets
YouTube: Coding Droplets
Website: codingdroplets.com






