Skip to content

MediatR

Developer UtilitiesUtilitiesC#

What it is

MediatR implements the mediator pattern for .NET, dispatching requests to handlers and decoupling callers from implementations, with a pipeline for cross-cutting behaviour.

Define a request and a handler. Controllers send the request rather than calling a service, and pipeline behaviours wrap every handler for logging, validation or transactions.

Installation

dotnet add package MediatR

Getting started

The smallest useful thing you can do with it, and what each part means.

Request, handler and dispatch
public record GetBookQuery(int Id) : IRequest<BookDto?>;

public class GetBookHandler : IRequestHandler<GetBookQuery, BookDto?>
{
    private readonly AppDb _db;
    public GetBookHandler(AppDb db) => _db = db;

    public async Task<BookDto?> Handle(GetBookQuery request, CancellationToken ct) =>
        await _db.Books
            .Where(b => b.Id == request.Id)
            .Select(b => new BookDto(b.Id, b.Title))
            .FirstOrDefaultAsync(ct);
}

app.MapGet("/books/{id}", async (int id, ISender sender) =>
{
    var book = await sender.Send(new GetBookQuery(id));
    return book is null ? Results.NotFound() : Results.Ok(book);
});
The endpoint depends only on ISender, so each use case lives in its own handler class rather than a service that grows to two thousand lines.

Advanced usage

Where the library earns its place over a simpler alternative.

Pipeline behaviours
public class ValidationBehaviour<TReq, TRes> : IPipelineBehavior<TReq, TRes>
    where TReq : notnull
{
    private readonly IEnumerable<IValidator<TReq>> _validators;

    public async Task<TRes> Handle(TReq request,
        RequestHandlerDelegate<TRes> next, CancellationToken ct)
    {
        var failures = (await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(request, ct))))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToList();

        if (failures.Count > 0) throw new ValidationException(failures);
        return await next();
    }
}

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
One behaviour validates every request in the application. This is MediatR's real payoff — cross-cutting concerns applied once rather than repeated in each handler.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

No handler was found for request of type
The assembly containing handlers was not registered. Check AddMediatR points at the right assembly.
Behaviours do not run
Open generic registration is required: typeof(IPipelineBehavior<,>), not a closed type.

Best practices

  • Use records for requests; they are immutable and give value equality for free.
  • Put cross-cutting concerns in behaviours rather than repeating them in handlers.
  • Keep handlers thin and focused on one use case.
  • Consider whether the indirection is worth it — for a small CRUD API, calling a service directly is simpler.

Background

Why it exists, and what it was reacting to.

Created by Jimmy Bogard, MediatR became the backbone of CQRS-style .NET architectures, where each use case is a request type with its own handler.