Requests, handlers, pipeline behaviors, notifications: the mediator pattern as MediatR popularized it is a comfortable way to structure a .NET application. After using it on several projects, I kept adding the same things around it: tracing, a way to send some notifications to a message broker, a check that no request had two handlers.
AnyMediator is the result: an in-process mediator with the same type names and configuration API as MediatR, and those things built in. This post shows what it looks like and what it adds.
Disclosure: I am the author.
The basics look familiar
dotnet add package AnyMediator builder.Services.AddAnyMediator(cfg => { cfg.RegisterServicesFromAssemblyContaining<Program>(); cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); }); public sealed record GetUser(int Id) : IRequest<UserDto>; public sealed class GetUserHandler(AppDbContext db) : IRequestHandler<GetUser, UserDto> { public async Task<UserDto> Handle(GetUser request, CancellationToken cancellationToken) => await db.Users .Where(u => u.Id == request.Id) .Select(u => new UserDto(u.Id, u.Name)) .SingleAsync(cancellationToken); } app.MapGet("/users/{id}", (int id, ISender sender) => sender.Send(new GetUser(id))); IRequest, IRequestHandler, INotification, INotificationHandler, IPipelineBehavior, ISender, IPublisher, IMediator, stream requests, pre/post-processors, exception handlers and actions: if you know MediatR, you already know the API. It targets .NET 8 and .NET 10.
Tracing without writing a behavior
Every Send, CreateStream and Publish is a span, and every notification handler is a child span, through .NET's own ActivitySource. Add the source to OpenTelemetry and they appear in your traces:
builder.Services.AddOpenTelemetry() .WithTracing(t => t .AddSource(AnyMediatorDiagnostics.ActivitySourceName) .AddAspNetCoreInstrumentation() .AddOtlpExporter()) .WithMetrics(m => m .AddMeter(AnyMediatorDiagnostics.MeterName) .AddOtlpExporter()); A request to GET /users/42 then shows the ASP.NET Core span, send GetUser under it, and the database spans under that. A failure sets the span status to Error with the exception, following the OpenTelemetry conventions. The anymediator.operation.duration histogram gives the duration, count and failures per message type.
There is nothing to install and no option to turn on. With nobody listening, the cost is a couple of flag checks.
Notifications that leave the process
Publish hands each notification to every registered INotificationSink. The default sink runs your in-process handlers; add another one and the same Publish also reaches a message broker, a webhook or an outbox:
public sealed class ServiceBusSink(ServiceBusSender sender) : INotificationSink { public Task Dispatch<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification => sender.SendMessageAsync( new ServiceBusMessage(BinaryData.FromObjectAsJson(notification)) { Subject = typeof(TNotification).Name }, cancellationToken); } builder.Services.AddSingleton<INotificationSink, ServiceBusSink>(); Handlers do not know or care where else the notification goes. Every sink runs even if an earlier one fails, and the failures reach the caller afterwards.
Mistakes caught at startup
If assembly scanning finds two classes handling the same request, AddAnyMediator throws and names both of them, instead of picking one silently. The usual culprit is a test double left in a scanned assembly, and it is much nicer to find out when the app starts than when a request returns the wrong result.
When a request has no handler at all, the error message says which interface to implement and which assembly to scan.
Fast where it counts
BenchmarkDotNet on .NET 10, one handler, no behaviors, each library with its default registration:
| Library | Send | Allocated | Publish | Allocated |
|---|---|---|---|---|
| MediatR 12.5.0 | 92 ns | 200 B | 145 ns | 288 B |
| AnyMediator | 86 ns | 96 B | 90 ns | 80 B |
| Mediator 3.0.2 (source generated) | 11 ns | 0 B | 18 ns | 0 B |
Dispatch goes through small generic wrappers built once per message type, so there is no reflection after the first call. Martin Othamar's Mediator is in another class thanks to its source generator; for a request that touches a database or the network, none of these differences is measurable.
Coming from MediatR
Handlers and behaviors compile unchanged. The migration is the package, the namespaces and the registration call:
- using MediatR; + using AnyMediator; - services.AddMediatR(cfg => + services.AddAnyMediator(cfg => { - cfg.LicenseKey = "..."; cfg.RegisterServicesFromAssemblyContaining<Program>(); }); The migration guide lists the few differences, and the docs home page has a converter you can paste your Program.cs into. AnyMediator is Apache-2.0, with no license key.
When to pick something else
- Native AOT, or every nanosecond counts: Mediator and its source generator.
- Queues, retries and durable messaging as a framework: Wolverine.
- Happy with MediatR as it is: no reason to move.
The docs have a side-by-side comparison of AnyMediator, FreeMediator, MediatR, Mediator, LiteBus and Wolverine, with sources.
Links
- Documentation: https://adiakys.github.io/AnyMediator/
- NuGet: https://www.nuget.org/packages/AnyMediator
- GitHub: https://github.com/Adiakys/AnyMediator
Feedback, issues and pull requests are welcome, especially from anyone who tries it on a real codebase.

