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
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.