Skip to content

Mediator

The mediator dispatches typed requests to their registered handlers in-process. It uses the same middleware pipeline model as the rest of Emit, so tracing, metrics, observers, and custom middleware all work without extra wiring.

Registration

Program.cs
builder.Services.AddEmit(emit =>
{
emit.AddMediator(mediator =>
{
mediator.AddHandler<PlacePizzaOrderHandler>();
mediator.AddHandler<GetPizzaStatusHandler>();
});
});

AddMediator is an extension on EmitBuilder. You can call it alongside AddKafka or on its own; the two are independent.

Requests

A request is a plain class or record that implements IRequest (fire-and-forget) or IRequest<TResponse> (returns a value):

public record PlacePizzaOrder(
string CustomerId,
string[] Toppings,
string Address) : IRequest;
public record GetPizzaStatus(Guid PizzaId) : IRequest<PizzaStatusDto>;

Handlers

Implement IRequestHandler<TRequest> or IRequestHandler<TRequest, TResponse>:

public class PlacePizzaOrderHandler(IPizzaRepository pizzas)
: IRequestHandler<PlacePizzaOrder>
{
public async Task HandleAsync(
PlacePizzaOrder request,
CancellationToken cancellationToken)
{
await pizzas.SaveAsync(
new PizzaOrder(Guid.NewGuid(), request.CustomerId, request.Toppings, request.Address),
cancellationToken);
}
}
public class GetPizzaStatusHandler(IPizzaRepository pizzas)
: IRequestHandler<GetPizzaStatus, PizzaStatusDto>
{
public async Task<PizzaStatusDto> HandleAsync(
GetPizzaStatus request,
CancellationToken cancellationToken)
{
var order = await pizzas.FindAsync(request.PizzaId, cancellationToken);
return new PizzaStatusDto(order.PizzaId, order.Status, order.EstimatedDelivery);
}
}

Handlers are resolved from the DI container, so constructor injection works normally.

Dispatching

Inject IMediator wherever you need it:

public class PizzeriaService(IMediator mediator)
{
public Task PlaceAsync(PlacePizzaOrderRequest req, CancellationToken ct)
=> mediator.SendAsync(new PlacePizzaOrder(req.CustomerId, req.Toppings, req.Address), ct);
public Task<PizzaStatusDto> GetStatusAsync(Guid pizzaId, CancellationToken ct)
=> mediator.SendAsync<PizzaStatusDto>(new GetPizzaStatus(pizzaId), ct);
}

Sending to an unregistered request type throws InvalidOperationException at dispatch time.

Middleware

The mediator supports the same middleware levels as the rest of Emit. Register mediator-wide middleware on InboundPipeline, or per-handler middleware using the two-argument AddHandler overload:

emit.AddMediator(mediator =>
{
// Runs for every request dispatched through this mediator
mediator.InboundPipeline.Use<LoggingMiddleware>();
// Runs only for PlacePizzaOrder requests
mediator.AddHandler<PlacePizzaOrderHandler, PlacePizzaOrder>(handler =>
{
handler.Use<ValidationMiddleware>();
});
mediator.AddHandler<GetPizzaStatusHandler>();
});

The context flowing through all middleware layers is MediatorContext<TRequest>, which extends MessageContext<T>.

Observers

IMediatorObserver provides lifecycle hooks without writing middleware. An observer receives three callbacks: OnHandlingAsync (before the handler runs), OnHandledAsync (after successful completion), and OnHandleErrorAsync (on exception):

public class AuditObserver : IMediatorObserver
{
public Task OnHandlingAsync<T>(MediatorContext<T> context)
{
return Task.CompletedTask;
}
public Task OnHandledAsync<T>(MediatorContext<T> context)
{
return Task.CompletedTask;
}
public Task OnHandleErrorAsync<T>(MediatorContext<T> context, Exception exception)
{
return Task.CompletedTask;
}
}

Register observers on the mediator builder:

mediator.AddObserver<AuditObserver>();

Observer exceptions are swallowed and logged; a failing observer never interrupts the handler.

Transactional handlers

Mediator handlers participate in the transactional outbox by decorating the handler class with [Transactional]. See the Transactional Outbox page for full details on what that implies for transaction scope and retry behavior.

[Transactional]
public sealed class CreateOrderHandler(
IEventProducer<string, OrderCreated> producer,
MyDbContext db) : IRequestHandler<CreateOrderCommand>
{
public async Task HandleAsync(CreateOrderCommand cmd, CancellationToken ct)
{
db.Orders.Add(cmd.Order);
await producer.ProduceAsync(..., ct);
// SaveChanges and transaction commit are handled by middleware.
}
}

One handler per request type

Registering two handlers for the same request type throws at startup. If you need fan-out, dispatch to multiple targets inside a single handler, or publish an event through the outbox.