HT
How Things Work
System Online

Mediator & MediatR

Decoupling senders from handlers through a central dispatcher, with pipeline behaviors for cross-cutting concerns — the backbone of thin controllers and clean request handling in .NET.

How It Works

The Mediator pattern routes communication through a central object so collaborators don't reference each other directly. In .NET, MediatR popularized it for application architecture: each request maps to one handler, controllers just send requests, and pipeline behaviors wrap every handler with cross-cutting concerns like validation, logging, and transactions — keeping business logic clean and dependencies loose.

1
The Mediator Pattern

Instead of objects calling each other directly (an N×N web of dependencies), they communicate through a mediator. Senders publish a request; the mediator routes it to the right handler. This collapses many-to-many coupling into many-to-one, so components don't need references to each other.

2
Requests and Handlers

Each request type (CreateOrderCommand, GetOrderQuery) maps to exactly one handler. The sender (e.g. a controller) just calls mediator.Send(request) and gets a response — it has no idea which class handles it. Adding a feature means adding a request + handler pair, with no changes to callers.

3
Pipeline Behaviors

The mediator wraps every handler in a pipeline of behaviors — logging, validation, caching, transactions, performance timing. Each behavior runs before and after the handler (like middleware), so cross-cutting concerns are written once and applied everywhere, instead of being copy-pasted into every handler.

4
Short-Circuiting

A behavior can stop the pipeline before the handler runs. A ValidationBehavior that finds an invalid request throws (or returns a failure) immediately, so the handler is never invoked. This keeps validation, authorization, and caching out of the business logic entirely.

Key Concepts

🔀Mediator

A central object that routes requests to handlers, so senders and handlers don't reference each other.

📨Request / Handler

Each request type maps to exactly one handler containing the business logic for it.

🧅Pipeline Behavior

Middleware-like wrappers that run around every handler for cross-cutting concerns (logging, validation).

🪶Thin Controllers

Controllers just send a request and return the result — no business logic, no handler knowledge.

📢Notification (Pub/Sub)

A one-to-many message handled by zero or more handlers — for in-process domain events.

🧩Decoupling

The mediator turns many-to-many dependencies into many-to-one, drastically reducing coupling.

Request, handler & pipeline behavior
tsx
1// A request and its single handler (MediatR style).
2public record CreateOrder(string[] Items) : IRequest<Guid>;
3
4public class CreateOrderHandler : IRequestHandler<CreateOrder, Guid> {
5 public async Task<Guid> Handle(CreateOrder cmd, CancellationToken ct) {
6 var order = Order.Create(cmd.Items); // just the business logic
7 await _repo.AddAsync(order, ct);
8 return order.Id;
9 }
10}
11
12// Cross-cutting concerns become pipeline behaviors that wrap EVERY handler:
13public class ValidationBehavior<TReq, TRes> : IPipelineBehavior<TReq, TRes> {
14 public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct) {
15 Validate(req); // throws → handler never runs (short-circuit)
16 return await next(); // call the next behavior / the handler
17 }
18}
19
20// The controller stays thin — it knows nothing about handlers:
21[HttpPost] public Task<Guid> Post(CreateOrder cmd) => _mediator.Send(cmd);
💡
Why This Matters

Mediator (via MediatR) is ubiquitous in modern .NET, especially alongside CQRS and clean architecture, and it's a common interview and code-review topic. It delivers thin controllers, centralized cross-cutting concerns, and loose coupling — but it can also add indirection and 'magic', so knowing when it helps versus when it obscures is the real skill.

Common Pitfalls

Over-using it for trivial apps — the indirection (find-the-handler) adds friction with little payoff.
Hiding so much in behaviors that control flow becomes hard to follow ('spooky action at a distance').
Putting real business logic in pipeline behaviors instead of handlers — behaviors are for cross-cutting concerns.
One giant handler doing many things — handlers should stay focused, like single-responsibility use cases.
Treating the mediator as a service locator, resolving arbitrary dependencies through it.
Real-World Use Cases

1Fat Controllers Bloated With Cross-Cutting Code

Scenario

Every controller action repeats the same boilerplate: log the request, validate the DTO, start a transaction, call services, handle errors, log the response. The controllers are huge and the duplication is everywhere.

Problem

Cross-cutting concerns are copy-pasted into every action, and controllers mix HTTP concerns with business orchestration. A change to logging or validation policy means editing dozens of files, and it's easy to miss one.

Solution

Introduce a mediator: each action becomes mediator.Send(command). Logging, validation, and transaction handling move into pipeline behaviors that wrap every handler automatically. Controllers shrink to one line; the cross-cutting logic lives in exactly one place.

💡

Takeaway: A mediator with pipeline behaviors centralizes cross-cutting concerns and slims controllers to dispatch-only. Write logging/validation/transactions once and they apply to every request consistently.

2Tangled Service-to-Service Dependencies

Scenario

OrderService depends on InventoryService, EmailService, and AuditService; those depend on others. The dependency graph is a tangled web, constructors have ten parameters, and it's hard to test or change anything in isolation.

Problem

Direct object-to-object calls created dense many-to-many coupling. Each new interaction added another reference, constructors ballooned, and a change in one service rippled across many.

Solution

Route interactions through the mediator. When an order is placed, the handler sends commands or publishes an OrderPlaced notification; inventory, email, and audit handlers react independently. Services no longer hold references to each other — they depend only on the mediator and their own requests.

💡

Takeaway: Use a mediator to break a many-to-many dependency web into independent request/handler pairs. Components become individually testable and changeable, communicating through messages rather than hard references.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.