MediatR — Guía de uso
🌍 Idioma: English | Español
Tabla de contenidos
- Tipos de mensaje
- Definir mensajes
- Implementar handlers
- API de IMediator
- IMediatorHelper — respuestas ARC
- Registrar handlers
- Registro y auto-escaneo
- Atributo MediatorAbstract
- Behaviors de pipeline
- Referencia de clases base
Tipos de mensaje
| Interfaz | Clase base | Interfaz handler | Clase base handler |
|---|---|---|---|
IRequest |
TRequest |
IRequestHandler<T> |
TRequestHandler<T> |
IRequest<TResponse> |
TRequest<TResponse> |
IRequestHandler<TRes,TReq> |
TResponseHandler<TRes,TReq> |
INotification |
TNotification |
INotificationHandler<T> |
TNotificationHandler<T> |
Definir mensajes
Comando (sin valor de retorno)
uses Daf.MediatR.Contracts;
type
TDeleteUserCommand = class(TRequest)
UserId: Integer;
end;Consulta (con valor de retorno)
type
TGetUserQuery = class(TRequest<TUserDTO>)
UserId: Integer;
end;Notificación (evento)
type
TUserDeletedEvent = class(TNotification)
UserId: Integer;
DeletedAt: TDateTime;
end;Implementar handlers
Handler de comando
type
TDeleteUserHandler = class(TRequestHandler<TDeleteUserCommand>)
private
FRepo: IUserRepository;
public
constructor Create(Repo: IUserRepository);
procedure Handle(Request: TDeleteUserCommand); override;
end;
procedure TDeleteUserHandler.Handle(Request: TDeleteUserCommand);
begin
FRepo.Delete(Request.UserId);
end;Handler de consulta
type
TGetUserHandler = class(TResponseHandler<TUserDTO, TGetUserQuery>)
private
FRepo: IUserRepository;
public
constructor Create(Repo: IUserRepository);
procedure Handle(Request: TGetUserQuery; out Result: TUserDTO); override;
end;
procedure TGetUserHandler.Handle(Request: TGetUserQuery; out Result: TUserDTO);
begin
Result := FRepo.FindById(Request.UserId);
end;La respuesta sale por un parámetro out, no como resultado de la función — de ahí la recoge el mediador.
Handler de notificación
type
TEmailOnUserDeleted = class(TNotificationHandler<TUserDeletedEvent>)
procedure Handle(Notification: TUserDeletedEvent); override;
end;
procedure TEmailOnUserDeleted.Handle(Notification: TUserDeletedEvent);
begin
// enviar email de despedida
end;API de IMediator
IMediator es un record que envuelve al IMediatorImpl registrado. Resuelve la implementación y asígnala: la conversión implícita te entrega el record.
IMediator = record
// Comando — sin valor de retorno
procedure Send<TRequest: class, IRequest>(Request: TRequest); overload;
// Consulta — con valor de retorno
function Send<TResponse; TRequest: class, IRequest<TResponse>>(
Request: TRequest): TResponse; overload;
// Notificación — fan-out a todos los handlers registrados
procedure Publish<TNotification: class, INotification>(
Notification: TNotification);
end;Uso:
var Mediator: IMediator := Provider.GetRequiredService<IMediatorImpl>;
// Comando
var Cmd := TDeleteUserCommand.Create;
Cmd.UserId := 42;
Mediator.Send<TDeleteUserCommand>(Cmd);
// Consulta
var Q := TGetUserQuery.Create;
Q.UserId := 42;
var DTO := Mediator.Send<TUserDTO, TGetUserQuery>(Q);
// Notificación
var Evt := TUserDeletedEvent.Create;
Evt.UserId := 42;
Mediator.Publish<TUserDeletedEvent>(Evt);IMediatorHelper — respuestas ARC
IMediatorHelper es un record helper de IMediator, así que SendARC está sobre el propio mediador y no hay nada que resolver. Envuelve una respuesta de clase en ARC<TResponse>, que se libera al salir del scope:
IMediatorHelper = record helper for IMediator
function SendARC<TResponse: class; TRequest: class, IRequest<TResponse>>(
const Request: TRequest): ARC<TResponse>;
end;var Q := TGetOrdersQuery.Create;
var Orders := Mediator.SendARC<TOrderList, TGetOrdersQuery>(Q);
// Orders es ARC<TOrderList> — la lista se libera al salir Orders del scopeRegistrar handlers
Cada handler se registra como servicio transitorio en DI:
// Handler de comando
Services.AddTransient<IRequestHandler<TDeleteUserCommand>, TDeleteUserHandler>;
// Handler de consulta
Services.AddTransient<IRequestHandler<TUserDTO, TGetUserQuery>, TGetUserHandler>;
// Handler de notificación (se soportan múltiples handlers)
Services.AddTransient<INotificationHandler<TUserDeletedEvent>, TEmailOnUserDeleted>;
Services.AddTransient<INotificationHandler<TUserDeletedEvent>, TAuditLogHandler>;Registro y auto-escaneo
uses Daf.Rtti, Daf.MediatR.DependencyInjection;
Services.AddMediatR; // el mediador
Services.AddMediatRClasses(_T.PackageOf<TMyHandler>); // los handlers de ese paquete
Services.AddMediatRBehaviors(_T.PackageOf<TMyHandler>); // y sus behaviors de pipelineAddMediatR registra IMediatorImpl como transient, y solo si no lo había registrado nadie antes. No escanea nada: el descubrimiento es AddMediatRClasses(Package), que recorre el RTTI de ese paquete buscando interfaces handler y registra lo que encuentra — los handlers no tienen que estar antes en el contenedor.
⚠️ Los tres son métodos de un record helper sobre IServiceCollection, y solo un helper por tipo está en alcance. Si otro uses lo ensombrece, la clase MediatR cubre los dos primeros —MediatR.AddTo(Services) y MediatR.AddTo(Services, Package)— y los behaviors van de uno en uno por MediatR.AddBehavior(Services, TMiBehavior). No hay equivalente de clase para AddMediatRBehaviors.
Atributo MediatorAbstract
Marca las clases base de handler abstractas con [MediatorAbstract] para que el escaneo de DI las salte: AddMediatRClasses y AddMediatRBehaviors no registran nunca una clase que lo lleve, que es lo que evita pedirle al contenedor una clase abstracta. El registro a mano ignora el atributo — solo lo leen los escaneos.
[MediatorAbstract]
TBaseOrderHandler<T: TRequest> = class(TRequestHandler<T>)
// lógica compartida
end;
TCreateOrderHandler = class(TBaseOrderHandler<TCreateOrderCommand>)
// concreto — NO marcado con MediatorAbstract
end;Behaviors de pipeline
Los behaviors interceptan cada llamada a Send y permiten añadir lógica transversal (logging, validación, caché, gestión de errores) sin modificar los handlers.
Tres variantes de behavior
Todas usan el mismo patrón Next.Call; el tipo de Next varía según la variante:
| Clase base | Se aplica a | Tipo de Next |
Sobreescribe |
|---|---|---|---|
TPipelineBehavior |
toda request | TNextValue |
function Handle(Request: TObject; Next: TNextValue): TValue |
TPipelineBehavior<TReq> |
un tipo de request void | TNext |
procedure Handle(Request: TReq; Next: TNext) |
TPipelineBehavior<TRes, TReq> |
un tipo de request con respuesta | TNext<TRes> |
function Handle(Request: TReq; Next: TNext<TRes>): TRes |
Behavior global (todas las requests)
type
TLoggingBehavior = class(TPipelineBehavior)
public
function Handle(Request: TObject; Next: TNextValue): TValue; override;
end;
function TLoggingBehavior.Handle(Request: TObject; Next: TNextValue): TValue;
begin
Log('Antes de ' + Request.ClassName);
Result := Next.Call;
Log('Después de ' + Request.ClassName);
end;Behavior tipado void (un tipo IRequest)
type
TValidationBehavior = class(TPipelineBehavior<TCreateOrderCommand>)
public
procedure Handle(Request: TCreateOrderCommand; Next: TNext); override;
end;
procedure TValidationBehavior.Handle(Request: TCreateOrderCommand; Next: TNext);
begin
if Request.Name.IsEmpty then
Exit; // cortocircuito — el handler nunca se llama
Next.Call;
end;Behavior tipado con respuesta (un tipo IRequest<TResponse>)
type
TValidatePingBehavior = class(TPipelineBehavior<string, TPing>)
public
function Handle(Request: TPing; Next: TNext<string>): string; override;
end;
function TValidatePingBehavior.Handle(Request: TPing; Next: TNext<string>): string;
begin
if Request.Target.IsEmpty then
raise EArgumentException.Create('Target requerido');
Result := Next.Call;
end;El framework usa RTTI para determinar si un behavior aplica al tipo de request actual. Los behaviors tipados también se aplican a subclases.
Cortocircuito
No llamar a Next.Call aborta el pipeline:
procedure TValidationBehavior.Handle(Request: TCreateOrderCommand; Next: TNext);
begin
if not IsValid(Request) then
Exit; // el handler nunca se llama
Next.Call;
end;Para behaviors globales devuelve Default(TValue) en lugar de llamar a Next.Call:
function TAuthBehavior.Handle(Request: TObject; Next: TNextValue): TValue;
begin
if not Authenticated then
Result := Default(TValue)
else
Result := Next.Call;
end;Gestión de excepciones
Envuelve Next.Call para capturar errores de behaviors internos y del handler:
function TErrorBehavior.Handle(Request: TObject; Next: TNextValue): TValue;
begin
try
Result := Next.Call;
except on E: Exception do
Result := Default(TValue);
end;
end;Inyección de dependencias
Los parámetros del constructor se resuelven del contenedor automáticamente:
constructor TLoggingBehavior.Create(const Logger: ILogger);
begin
inherited Create;
FLogger := Logger;
end;Registro
// Auto-descubrimiento (omite clases marcadas [MediatorAbstract])
ServiceCollection.AddMediatRBehaviors(_T.PackageOf<TMyClass>);
// Manual
MediatR.AddBehavior(ServiceCollection, TLoggingBehavior);
MediatR.AddBehavior(ServiceCollection, TValidatePingBehavior);⚠️ El escaneo de behaviors es AddMediatRBehaviors, y AddMediatRClasses no lo es: este descubre solo handlers, así que un paquete registrado únicamente con él no aporta ningún behavior — y sin error que lo delate, porque una request sin behaviors registrados va directa a su handler.
Los behaviors se ejecutan en orden de registro — el primero registrado es el más externo. Para behaviors que solo registras manualmente, márcalos con [MediatorAbstract] para excluirlos del auto-descubrimiento.
Referencia de clases base
| Clase base | Sobreescribe | ¿Llamar inherited? |
|---|---|---|
TRequestHandler<TReq> |
procedure Handle(Request: TReq) |
No |
TResponseHandler<TRes, TReq> |
procedure Handle(Request: TReq; out Result: TRes) |
No |
TNotificationHandler<TNot> |
procedure Handle(Notification: TNot) |
No |
TRequest |
— | Hereda IRequest |
TRequest<TResponse> |
— | Hereda IRequest<TResponse> |
TNotification |
— | Hereda INotification |
TPipelineBehavior |
function Handle(Request: TObject; Next: TNextValue): TValue |
No |
TPipelineBehavior<TReq> |
procedure Handle(Request: TReq; Next: TNext) |
No |
TPipelineBehavior<TRes, TReq> |
function Handle(Request: TReq; Next: TNext<TRes>): TRes |
No |