If you have built anything with the mediator pattern in .NET, you have almost certainly used MediatR. It is the de-facto way to keep controllers thin, decouple senders from handlers, and slot cross-cutting concerns into a clean pipeline.
But MediatR resolves and dispatches handlers using runtime reflection. That is fine for most apps — until you care about cold-start latency, allocation pressure on a hot path, or Native AOT and trimming, where reflection is exactly what you want to avoid.
So I built FastMediatR: a high-performance, AOT-friendly, MIT-licensed in-process mediator for .NET 10 whose public API is a near drop-in replacement for MediatR — but which moves all the dispatch wiring to compile time using a Roslyn source generator.
The idea
The core insight is simple: the mapping from a request type to its handler is fully known at compile time. There is no reason to discover it at runtime with MethodInfo, dynamic, or assembly scanning.
FastMediatR ships a Roslyn IIncrementalGenerator that, at build time, emits:
- a
switch-based dispatcher that routes each request to its handler with no reflection, and - the DI registration code that wires everything up.
The result is a mediator where Send and Publish are essentially a generated method call — faster, with fewer allocations, and fully compatible with Native AOT and trimming.
MediatR (runtime) FastMediatR (compile time)
┌────────────────────────────┐ ┌────────────────────────────────┐
│ Send(request) │ │ Send(request) │
│ → reflect over types │ │ → generated switch dispatch │
│ → build handler wrapper │ │ → direct handler call │
│ → invoke via MethodInfo │ │ (no reflection on hot path) │
└────────────────────────────┘ └────────────────────────────────┘
What you get
- Zero-reflection dispatch. A source generator emits a
switch-based dispatcher at compile time — noMethodInfo, nodynamic, no runtime type scanning on the hot path. - Native AOT & trimming friendly. The library ships
IsAotCompatible=trueandIsTrimmable=true. The only reflection touchpoint is a one-time startup lookup, and it has a fully trim-safe alternative you can call directly. - Near drop-in for MediatR.
IRequest<T>,IRequestHandler<,>,INotification,INotificationHandler<>,ISender,IPublisher,IMediator, andIPipelineBehavior<,>mirror the MediatR surface. For the 95% case, migration is changing ausingand swapping one registration call. - Build-time wiring validation. Diagnostics fail the build before you run:
FM001(no handler for a request),FM002(duplicate handler), andFM003(non-default handler lifetime). No more "handler not registered" surprises at runtime. - Built-in pipeline behaviors.
ValidationBehavior(FluentValidation),LoggingBehavior, andPerformanceBehavior, plus request pre-/post-processors. - Configurable notification publishing.
TaskWhenAllPublisher(default, concurrent fan-out) andSequentialAwaitPublisher(ordered, fail-fast) — or bring your own strategy. - Optional OpenTelemetry tracing via a separate
FastMediatR.Telemetrypackage, with a zero-overhead fast path when nobody is listening and PII-safe spans by default.
Why compile-time matters
Three benefits fall out of moving the work to build time:
- Performance. Dispatch is a generated direct call instead of a reflective invocation, and the pipeline short-circuits straight to the handler with zero closure allocations when no behaviors are registered.
- AOT & trimming safety. Reflection is the enemy of trimming and Native AOT. Because FastMediatR's hot path has none, your app trims cleanly and starts fast — ideal for containers, serverless, and CLI tools.
- Errors at build time, not 3 a.m. Forgetting to register a handler, or accidentally registering two, is caught by
FM001/FM002as a compile error. The feedback loop moves left.
How to use it
1. Define requests and handlers
using FastMediatR;
// Command that returns a value
public sealed record CreateUserCommand(string Email, string DisplayName) : IRequest<Guid>;
public sealed class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
public Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
=> Task.FromResult(Guid.NewGuid());
}
Notifications work the same way, with zero, one, or many subscribers:
public sealed record UserCreatedNotification(Guid UserId, string Email) : INotification;
public sealed class SendWelcomeEmail : INotificationHandler<UserCreatedNotification>
{
public Task Handle(UserCreatedNotification n, CancellationToken ct) { /* ... */ return Task.CompletedTask; }
}
2. Register FastMediatR
using FastMediatR.Extensions.DI;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastMediatR<Program>();
var app = builder.Build();
The Roslyn generator emits the dispatch table at compile time, so there is no reflection at runtime.
3. Dispatch from your endpoints
app.MapPost("/users", async (CreateUserCommand command, IMediator mediator, CancellationToken ct) =>
{
var id = await mediator.Send(command, ct);
return Results.Created($"/users/{id}", new { id });
});
app.MapPost("/users/{id:guid}/events/created",
async (Guid id, string email, IMediator mediator, CancellationToken ct) =>
{
await mediator.Publish(new UserCreatedNotification(id, email), ct);
return Results.Accepted();
});
That is the whole loop: define, register, dispatch. Inject ISender or IPublisher directly when a class only needs one half of the surface.
4. Add cross-cutting behavior
Pipeline behaviors wrap the handler — perfect for validation, logging, transactions, or metrics:
public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
// before
var response = await next(ct);
// after
return response;
}
}
Register it as an open generic and it applies to every request. FastMediatR also ships ready-made ValidationBehavior, LoggingBehavior, and PerformanceBehavior so you do not have to write them.
Migrating from MediatR
For most codebases the migration is mechanical:
| MediatR | FastMediatR |
|---|---|
using MediatR; |
using FastMediatR; |
services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>()) |
services.AddFastMediatR<Program>() |
IMediator / ISender / IPublisher |
identical |
IRequest<T>, IRequestHandler<,>, INotification, IPipelineBehavior<,> |
identical |
Your handlers, notifications, and behaviors compile and behave the same. There are a couple of intentional differences (for example, FastMediatR models void commands as IRequest : IRequest<Unit> so the generator can emit a single uniform dispatch path), but they only matter if you write a custom notification publisher or depend on a void handler returning a plain Task.
When not to reach for it
FastMediatR is an in-process mediator. It deliberately does not do:
- cross-process or distributed messaging — use MassTransit or Wolverine;
- sagas, workflow orchestration, or behavior trees;
- general-purpose IoC — it composes on top of
Microsoft.Extensions.DependencyInjection.
If you want a fast, allocation-light, AOT-ready in-process mediator with the MediatR programming model you already know, that is exactly the gap FastMediatR fills.
Status & where to find it
FastMediatR is pre-1.0 and under active development — the public API may shift until v1.0.0 is tagged. It targets .NET 10 and is MIT-licensed.
If the idea of "the same MediatR ergonomics, but the dispatch is generated at compile time and it runs under Native AOT" sounds useful to you, give it a try and let me know what you think.
Happy dispatching.
💬 Comments & Reactions