MediatR shows up in a lot of .NET codebases by default now. Someone starts a project, adds it in the first week, and every request goes through a handler from then on. Ask why it’s there and the answer is usually “it’s clean” or “it’s what we always do.” That’s a habit, not a reason.
For a lot of apps, MediatR adds extra layers that cost you more than you get back.
What People Think It Gives Them
The pitch is decoupling. Your controller doesn’t call a service directly — it sends a request object, and somewhere a handler picks it up and deals with it. The controller doesn’t know who handles it. On paper that sounds like clean separation.
The other selling point is the pipeline. You can wrap every request in behaviors — logging, validation, transactions — without touching each handler. One place to add the stuff that cuts across everything.
Both are real features. Whether you’re actually getting the benefit, or just paying for the setup, is the part worth thinking about.
What It Actually Costs
Start with navigation. In a normal service call you hit F12 on the method and you’re looking at the code. With MediatR you hit F12 on Send() and you land inside the library. To find the handler you go searching by the request type name, because there’s no link the compiler can follow between the thing sending the request and the thing handling it. Across a whole codebase, every developer pays that small tax every time they try to follow the flow.
Then there’s the extra code. A request that could’ve been _orderService.Cancel(id) becomes a command class, a handler class, a registration, and a Send() call. You’ve turned one method into four moving parts. For a genuinely complex operation that might be worth it. For “cancel an order,” it’s a lot of code for nothing.
And the decoupling often isn’t real. Your controller still needs the request object, the request object still goes to exactly one handler, and if you change what the operation does you change both. Nothing is actually swappable. You’ve got the extra layers of loose coupling with none of the flexibility, because each request has one handler and always will.
The Pipeline Argument Is the Strongest One
If there’s a reason to reach for MediatR, it’s the behaviors, so it’s worth being straight about it.
Wrapping every request in validation, logging, and transaction handling from one place is genuinely useful, and doing the same thing without MediatR takes more work. Middleware handles some of it, but not the per-request-type control you get from a pipeline behavior.
Here’s the catch. .NET already gives you most of this. Middleware covers the stuff that cuts across requests. Filters cover it at the action level. Validation has standard ways that don’t need a mediator at all. So the pipeline is a real benefit, but for a lot of apps it’s solving a problem the framework already handles.
When It Actually Earns Its Place
MediatR isn’t bad. It’s a well-built library, and there are codebases where it fits.
If you’ve really gone with CQRS and you have a proper split between commands and queries, each handled its own way, the request/handler model fits that. If you’ve got a large team and forcing one consistent shape for every operation is worth something, the uniformity helps. If your pipeline is complex enough that behaviors save real work, that’s a fair trade.
Those are specific cases. The problem isn’t people using MediatR there. It’s people adding it to a plain CRUD API on day one because it’s become the default, then living with the extra layers for the life of the project without ever needing what it offers.
Our Take
At Qodors, when we pick up an existing .NET codebase, MediatR is often there and often it’s doing nothing. Every endpoint sends a command, every command has one handler, and no behavior in the pipeline is doing anything a filter or a bit of middleware couldn’t. The abstraction is there, the benefit isn’t.
Newer developers feel it most. They open the project, see requests going off into handlers with no direct call to follow, and spend their first weeks learning the plumbing instead of the domain. That cost is real, and it gets paid on every codebase that added the library without needing it.
Before you add MediatR to a new project, ask what it does that a plain service class won’t. If you can point at a real pipeline need or an actual CQRS split, add it. If the answer is that it’s cleaner, look again. A service you can jump straight to beats a request you have to go hunting for.
Quick Checklist
Adding MediatR? Name the specific thing it does that a service class won’t
One handler per request forever isn’t decoupling — it’s an extra layer with no payoff
The stuff that cuts across requests often fits in middleware or filters you already have
Pipeline behaviors are the real benefit — reach for it when you actually need them
A real CQRS split or a big team enforcing one shape are fair reasons; “it’s clean” isn’t
Count the navigation cost — every developer pays it every time they trace a request
MediatR solves real problems. Most CRUD apps don’t have those problems. If you’re reaching for it out of habit instead of a reason you can name, a plain service class will serve you better and read clearer.
DotNet #CSharp #MediatR #SoftwareArchitecture #CQRS #BackendDevelopment #SoftwareEngineering #DotNetCore #CleanCode #QodorsEdge
Written by the team at Qodors — we untangle over-abstracted .NET codebases for a living. → www.qodors.com
답글 남기기