Mediator pattern in .NET

2 min read
On trails:EDANETstep 2/7

The mediator is a behavioral patternBehavioral patternsFour patterns for how objects interact and share out responsibilities.Opens in a new tab: objects talk to a mediator instead of to each other, and the mediator decides who handles the message. In .NET it is used inside one application, where it carries two kinds of message from events, commands and queriesEvents, commands and queriesThree kinds of message: an event states a fact, a command asks one owner to act, a query only reads.Opens in a new tab. A command goes to exactly one handler. A notification goes to zero or more handlers, and the sender does not know which.

In code

MediatR is the best-known library. Send dispatches a command to its single handler. Publish dispatches a notification to every handler registered for it. In the shop, PlaceOrder saves the order and then announces OrderPlaced, and two handlers react.

Handlers.csC#
public sealed record PlaceOrder(Guid CustomerId, decimal Total) : IRequest<Guid>; public sealed record OrderPlaced(Guid OrderId, decimal Total) : INotification; public sealed class PlaceOrderHandler(OrdersDb db, IMediator mediator)    : IRequestHandler<PlaceOrder, Guid>{    public async Task<Guid> Handle(PlaceOrder cmd, CancellationToken ct)    {        var order = Order.Create(cmd.CustomerId, cmd.Total);        db.Orders.Add(order);        await db.SaveChangesAsync(ct);         await mediator.Publish(new OrderPlaced(order.Id, cmd.Total), ct);        return order.Id;    }} public sealed class ReserveStock(IStock stock) : INotificationHandler<OrderPlaced>{    public Task Handle(OrderPlaced evt, CancellationToken ct) =>        stock.ReserveAsync(evt.OrderId, ct);} public sealed class SendConfirmation(IEmailSender email) : INotificationHandler<OrderPlaced>{    public Task Handle(OrderPlaced evt, CancellationToken ct) =>        email.SendConfirmationAsync(evt.OrderId, ct);}

The endpoint knows only IMediator and the request. It does not reference PlaceOrderHandler, ReserveStock or SendConfirmation.

Program.csC#
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblyContaining<PlaceOrder>());

app.MapPost("/orders", async (PlaceOrder cmd, IMediator mediator, CancellationToken ct) =>
{
    var orderId = await mediator.Send(cmd, ct);
    return Results.Created($"/orders/{orderId}", new { orderId });
});

Why use it

Each use case has one entry point and one small handler, which suits a codebase organised by feature. It also maps onto the command side of CQRSCQRSSeparate models for changing data and for reading it.Opens in a new tab: commands change state, queries read it, each with its own handler. The libraries add a pipeline around the handlers for validation, logging or transactions, so those concerns are written once.

Libraries

LibraryLicence, currentlyBeyond in-process
MediatRVersion 13 and later: RPL-1.5 or a commercial licence. A free Community licence covers small organisations (under $5M annual revenue, with further conditions), and no key is needed outside production. Earlier versions keep their open-source licences.Nothing. In-process only, with pipeline behaviors.
WolverineMITThe same handlers can also consume messages from RabbitMQ, Azure Service Bus, Kafka and others, with an outbox.
BrighterMITA command dispatcher that also works as an external bus with an outbox.
Hand-writtenYour ownA dictionary from message type to handler, resolved from the container.
Table 1. In-process mediators for .NET (licences as stated by each project)

Wolverine’s bus.InvokeAsync(command) runs the handler right now on the calling thread, which is the mediator use. Its PublishAsync sends to subscribers without waiting. Check the licence page of the version you choose before adopting a library, because MediatR’s terms changed with version 13.

The catch

A notification through a mediator stays inside the process and usually inside the request. The handlers run on the caller’s call, so a slow or failing handler affects the caller. With MediatR’s default publisher, notification handlers run one after another, and if one throws, the ones after it do not run. TaskWhenAllPublisher runs them together instead. Nothing is stored, so a crash loses any notification that was in flight. A notification that must reach another service has to leave the process through a broker: see the outboxOutbox and inboxWrite the event in the same transaction as the data, publish it from there, and deduplicate on the way in.Opens in a new tab. Finding who handles a message also takes a search, because the sender does not name its handlers.


Sources