MediatR: ¿abstracción útil o código difícil de seguir?
MediatR es casi estándar en .NET, pero agrega una indirección en cada llamada. Qué aporta, qué cuesta, su nueva licencia y qué alternativas más simples hay.
Un desarrollador nuevo en el proyecto quiere entender qué pasa cuando se crea un pedido. Abre el controlador y encuentra await _mediator.Send(new CreateOrderCommand(...)). Presiona F12 y llega a la definición del comando: un record sin lógica. Para encontrar el código que se ejecuta, tiene que buscar qué clase implementa IRequestHandler<CreateOrderCommand, ...>. Y para saber qué pasa antes y después, revisar qué behaviors están registrados en el contenedor.
Nada de esto es un problema grave por sí solo. Pero se repite en cada operación del sistema, y la pregunta es si lo que se gana compensa esa indirección.
Qué hace MediatR
Despacha un objeto (un request) al único handler registrado para ese tipo, pasando por una cadena de pipeline behaviors:
// El controlador no conoce el handler
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand command, CancellationToken ct)
{
var id = await mediator.Send(command, ct);
return CreatedAtAction(nameof(Get), new { id }, null);
}
// El handler hace el trabajo
public class CreateOrderHandler(AppDbContext db) : IRequestHandler<CreateOrderCommand, int>
{
public async Task<int> Handle(CreateOrderCommand request, CancellationToken ct) { ... }
}
Lo que realmente aporta
1. Pipeline behaviors: lógica transversal en un solo lugar
Es su mayor ventaja. Validación, logging, transacciones o métricas se aplican a todos los handlers sin repetirlas:
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var failures = validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.ToList();
if (failures.Count > 0) throw new ValidationException(failures);
return await next();
}
}
2. Una estructura uniforme
Cada caso de uso es un par request/handler. En equipos grandes, esa uniformidad hace que el código sea predecible: todos saben dónde buscar y cómo agregar algo nuevo.
3. Controladores delgados
El controlador solo traduce HTTP a un request. Aunque esto también se consigue inyectando un servicio.
Lo que cuesta
- Navegación indirecta. "Ir a la definición" no lleva al código que se ejecuta. Algunos IDE ayudan, pero la fricción existe en cada lectura.
- Errores en ejecución en lugar de compilación. Si falta el handler de un request, el compilador no avisa: falla cuando se llama.
- Orden de los behaviors implícito. Depende del orden de registro en el contenedor, que no se ve desde el handler.
- Tentación de encadenar handlers. Un handler que hace
mediator.Senda otro handler crea flujos difíciles de seguir y de depurar. - Licencia. Desde 2025, MediatR tiene licencia comercial para empresas por encima de cierto umbral de ingresos. Las versiones anteriores siguen disponibles, pero adoptarlo hoy en un proyecto nuevo implica revisar los términos.
La alternativa más simple: inyectar el handler
Si el valor principal es tener una clase por caso de uso, no hace falta un mediador:
public class CreateOrder(AppDbContext db, TimeProvider clock)
{
public async Task<int> ExecuteAsync(CreateOrderRequest request, CancellationToken ct) { ... }
}
// En Minimal APIs, inyectado directamente en el endpoint
app.MapPost("/api/orders", async (CreateOrderRequest request, CreateOrder useCase, CancellationToken ct) =>
{
var id = await useCase.ExecuteAsync(request, ct);
return Results.Created($"/api/orders/{id}", null);
});
F12 lleva directo al código. Si falta el registro, ValidateOnBuild lo detecta al arrancar. Y la lógica transversal se puede resolver con lo que ya ofrece ASP.NET Core:
- Validación con endpoint filters o filtros de MVC (lo vimos en middleware vs filters vs handlers).
- Logging y métricas con middleware u OpenTelemetry.
- Transacciones dentro del propio caso de uso, que es donde se conoce su alcance.
El trade-off
MediatR cambia navegabilidad por uniformidad y lógica transversal centralizada. En un equipo de 15 personas con cientos de casos de uso, esa uniformidad puede valer la pena. En un equipo de tres con 40 endpoints, la indirección suele costar más de lo que aporta.
Cuándo tiene sentido
- Equipos grandes que se benefician de una estructura rígida y uniforme.
- Muchos comportamientos transversales que se aplican a todos los casos de uso y no encajan en el pipeline HTTP (por ejemplo, porque los mismos casos de uso se ejecutan desde una API, una cola y un proceso por lotes).
- Proyectos que ya lo usan de forma consistente: quitarlo tiene un costo que pocas veces se justifica.
Cuándo evitarlo
- Proyectos nuevos y pequeños, donde un servicio o una clase por caso de uso es suficiente.
- Cuando solo se usa
Sendsin behaviors: es una llamada a un método con pasos extra. - Cuando se usa para "desacoplar" componentes que viven en el mismo proyecto y cambian juntos.
Conclusión
MediatR es útil cuando sus behaviors resuelven un problema real de lógica transversal. Como simple forma de llamar métodos, agrega indirección y, desde su cambio de licencia, una decisión comercial. Una clase por caso de uso, inyectada directamente, da la misma organización con mucho menos. Recuerda además que CQRS no depende de MediatR.
¿Tu sistema necesita una revisión de arquitectura?
Te ayudamos a decidir qué estructura necesita tu software hoy y cómo prepararlo para crecer, sin sobreingeniería.
Ver desarrollo a medida