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 — no MethodInfo, no dynamic, no runtime type scanning on the hot path.
  • Native AOT & trimming friendly. The library ships IsAotCompatible=true and IsTrimmable=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, and IPipelineBehavior<,> mirror the MediatR surface. For the 95% case, migration is changing a using and swapping one registration call.
  • Build-time wiring validation. Diagnostics fail the build before you run: FM001 (no handler for a request), FM002 (duplicate handler), and FM003 (non-default handler lifetime). No more "handler not registered" surprises at runtime.
  • Built-in pipeline behaviors. ValidationBehavior (FluentValidation), LoggingBehavior, and PerformanceBehavior, plus request pre-/post-processors.
  • Configurable notification publishing. TaskWhenAllPublisher (default, concurrent fan-out) and SequentialAwaitPublisher (ordered, fail-fast) — or bring your own strategy.
  • Optional OpenTelemetry tracing via a separate FastMediatR.Telemetry package, 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:

  1. 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.
  2. 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.
  3. Errors at build time, not 3 a.m. Forgetting to register a handler, or accidentally registering two, is caught by FM001/FM002 as 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.