Skip to main content

MediatR

🌍 Language: English | Español

In-process messaging for Delphi β€” commands, queries, and notifications dispatched through a single IMediator interface. Inspired by the .NET MediatR library.

Delphi 12+ License


Concepts

Type Direction Returns Use case
IRequest 1 β†’ 1 handler β€” Command (fire-and-forget)
IRequest<TResponse> 1 β†’ 1 handler TResponse Query
INotification 1 β†’ N handlers β€” Domain event

Quick Start β€” Command

// 1. Define command
type
  TCreateOrderCommand = class(TRequest)
    OrderId: Integer;
  end;

// 2. Implement handler
type
  TCreateOrderHandler = class(TRequestHandler<TCreateOrderCommand>)
    procedure Handle(Request: TCreateOrderCommand); override;
  end;

// 3. Register handler
Services.AddTransient<IRequestHandler<TCreateOrderCommand>, TCreateOrderHandler>;

// 4. Dispatch
Mediator.Send<TCreateOrderCommand>(Cmd);

Quick Start β€” Query

type
  TGetOrderQuery    = class(TRequest<TOrderDTO>);
  TGetOrderHandler  = class(TResponseHandler<TOrderDTO, TGetOrderQuery>)
    procedure Handle(Request: TGetOrderQuery; out Result: TOrderDTO); override;
  end;

var DTO := Mediator.Send<TOrderDTO, TGetOrderQuery>(Query);

Quick Start β€” Notification

type
  TOrderCreatedEvent    = class(TNotification);
  TEmailNotifyHandler   = class(TNotificationHandler<TOrderCreatedEvent>)
    procedure Handle(Notification: TOrderCreatedEvent); override;
  end;

// Register as many handlers as needed
Services.AddTransient<INotificationHandler<TOrderCreatedEvent>, TEmailNotifyHandler>;
Services.AddTransient<INotificationHandler<TOrderCreatedEvent>, TAuditLogHandler>;

// Publish β€” ALL handlers are called
Mediator.Publish<TOrderCreatedEvent>(Evt);

DI Registration

uses Daf.Rtti, Daf.MediatR.DependencyInjection;

Services.AddMediatR;                                   // registers IMediatorImpl as transient
Services.AddMediatRClasses(_T.PackageOf<TMyHandler>);  // discovers and registers handlers

Or manually per handler:

Services.AddTransient<IRequestHandler<TMyCommand>, TMyCommandHandler>;

Pipeline Behaviors

Behaviors wrap every Send call with cross-cutting logic (logging, validation, caching, etc.).

There are three behavior flavors β€” all use the same Next.Call pattern:

Base class Applies to Next type Override
TPipelineBehavior every request TNextValue function Handle(Request: TObject; Next: TNextValue): TValue
TPipelineBehavior<TReq> one void request type TNext procedure Handle(Request: TReq; Next: TNext)
TPipelineBehavior<TRes, TReq> one response request type TNext<TRes> function Handle(Request: TReq; Next: TNext<TRes>): TRes

Global behavior (all 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('Before ' + Request.ClassName);
  Result := Next.Call;
  Log('After ' + Request.ClassName);
end;

Typed void behavior (one IRequest type)

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;  // short-circuit β€” handler never called
  Next.Call;
end;

Typed response behavior (one IRequest<TResponse> type)

type
  TQueryResultBehavior = class(TPipelineBehavior<TOrderList, TGetOrdersQuery>)
  public
    function Handle(Request: TGetOrdersQuery; Next: TNext<TOrderList>): TOrderList; override;
  end;

function TQueryResultBehavior.Handle(Request: TGetOrdersQuery; Next: TNext<TOrderList>): TOrderList;
begin
  Result := Next.Call;
  Log(Format('%d order(s) returned', [Result.Count]));
end;

The framework uses RTTI to detect the concrete request type of Handle and skips the behavior for non-matching types automatically. Typed behaviors also apply to subclasses.

Registration

// Auto-discovery (scans package, skips [MediatorAbstract])
ServiceCollection.AddMediatRBehaviors(_T.PackageOf<TMyClass>);

// Manual
MediatR.AddBehavior(ServiceCollection, TLoggingBehavior);
MediatR.AddBehavior(ServiceCollection, TValidationBehavior);

Behaviors have their own scan: AddMediatRClasses discovers handlers and nothing else, so a package registered only through it contributes no behavior at all β€” and nothing reports it, because a request with no behaviors registered goes straight to its handler.

Execution order

Behaviors execute in registration order β€” first registered is the outermost wrapper:

Behavior1 β†’ Behavior2 β†’ Handler β†’ return to Behavior2 β†’ return to Behavior1

Short-circuiting

Don’t call Next.Call to abort the pipeline. The handler and inner behaviors are never invoked:

procedure TValidationBehavior.Handle(Request: TCreateOrderCommand; Next: TNext);
begin
  if not IsValid(Request) then
    Exit;  // handler is never called
  Next.Call;
end;

Exception handling

Wrap Next.Call in a try/except to catch exceptions from the handler or inner behaviors:

function TErrorBehavior.Handle(Request: TObject; Next: TNextValue): TValue;
begin
  try
    Result := Next.Call;
  except on E: Exception do
  begin
    Log('Error: ' + E.Message);
    Result := Default(TValue);
  end;
  end;
end;

Dependency injection

Behaviors are resolved from the DI container. Constructor parameters are injected automatically:

type
  TLoggingBehavior = class(TPipelineBehavior)
  private
    FLogger: ILogger;
  public
    constructor Create(const Logger: ILogger);
    function Handle(Request: TObject; Next: TNextValue): TValue; override;
  end;

Backward compatibility

Pipeline behaviors are opt-in. If no behaviors are registered, Send invokes the handler directly with no additional overhead.


Documentation

  • πŸ“– Usage Guide β€” IMediatorHelper, ARC responses, pipeline, [MediatorAbstract], DI scanning
  • πŸ“ MediatRSample β€” commands, queries and notifications in one program

Abstractions

MediatR.Abstractions

🌍 Language: English | Español

Core interfaces and base classes for the DAFce MediatR in-process messaging. Reference these from application code and handler implementations without taking a dependency on the concrete mediator.

All types live in Daf.MediatR.Contracts β€” the package is Daf.MediatR.Abstractions, which is not a unit and cannot be used in a uses clause.


Interfaces

IRequest

Fire-and-forget command β€” dispatched to exactly one handler, no return value.

IRequest = interface
end;

IRequest<TResponse>

Query β€” dispatched to exactly one handler, returns TResponse.

IRequest<TResponse> = interface(IRequest)
end;

INotification

Event β€” published to all registered handlers.

INotification = interface
end;

IMediator

IMediator = record            // a record, not an interface: it wraps an IMediatorImpl
  procedure Send<TRequest: class, IRequest>(Request: TRequest); overload;
  function  Send<TResponse; TRequest: class, IRequest<TResponse>>(
              Request: TRequest): TResponse; overload;
  procedure Publish<TNotification: class, INotification>(
              Notification: TNotification);
end;

IMediatorHelper

A record helper for IMediator, so SendARC sits on the mediator itself. Wraps a class response in ARC<TResponse>, released when it goes out of scope.

IMediatorHelper = record helper for IMediator
  function SendARC<TResponse: class; TRequest: class, IRequest<TResponse>>(
             const Request: TRequest): ARC<TResponse>;
end;

IRequestHandler<TRequest>

IRequestHandler<TRequest: class, IRequest> = interface
  procedure Handle(Request: TRequest);
end;

IRequestHandler<TResponse, TRequest>

The same name, two generic parameters: Delphi tells the two overloads apart by arity. The response comes back through an out parameter, not as the function result.

IRequestHandler<TResponse; TRequest: class, IRequest<TResponse>> = interface
  procedure Handle(Request: TRequest; out Result: TResponse);
end;

INotificationHandler<TNotification>

INotificationHandler<TNotification: class, INotification> = interface
  procedure Handle(Notification: TNotification);
end;

Base classes

Class Use
TRequest Inherit to define a command
TRequest<TResponse> Inherit to define a query
TNotification Inherit to define an event
TRequestHandler<TReq> Inherit to handle a command β€” override Handle
TResponseHandler<TRes,TReq> Inherit to handle a query β€” override Handle
TNotificationHandler<TNot> Inherit to handle an event β€” override Handle

Attributes

[MediatorAbstract]

Apply to abstract handler base classes so the DI scan skips them: AddMediatRClasses and AddMediatRBehaviors never register a class carrying it, which is what keeps the container from being asked to instantiate an abstract class. Without a scan the attribute does nothing.

[MediatorAbstract]
TBaseOrderHandler<T: TRequest> = class(TRequestHandler<T>);