A fast, zero-alloc Mediator pattern alternative to MediatR in .NET – minimal, blazing fast, and DI-friendly (Dependency Injection).
License
—
Deps
4
Install Size
—
Vulns
✓ 0
Published
Oct 8, 2025
$ dotnet add package DispatchR.Mediator** Minimal memory footprint. Blazing-fast execution. **
[!NOTE] If you're curious to see the power of this library, check out the benchmark comparing MediatR vs Mediator Source Generator vs DispatchR.
Task, ValueTask, or Synchronous MethodIRequest<TRquest, TResponse>IRequestHandler<TRequest, TResponse>IPipelineBehavior<TRequest, TResponse>IStreamRequest<TRquest, TResponse>IStreamRequestHandler<TRequest, TResponse>IStreamPipelineBehavior<TRequest, TResponse>INotificationINotificationHandler<TRequestEvent>:bulb: Tip: If you're looking for a mediator with the raw performance of hand-written code, DispatchR is built for you.
public sealed class PingMediatR : IRequest<int> { }
TRequest to IRequestasync and sync handlers
Task and ValueTaskpublic sealed class PingDispatchR : IRequest<PingDispatchR, ValueTask<int>> { }
public sealed class PingHandlerMediatR : IRequestHandler<PingMediatR, int>
{
public Task<int> Handle(PingMediatR request, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}
public sealed class PingHandlerDispatchR : IRequestHandler<PingDispatchR, ValueTask<int>>
{
public ValueTask<int> Handle(PingDispatchR request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(0);
}
}
public sealed class LoggingBehaviorMediat : IPipelineBehavior<PingMediatR, int>
{
public Task<int> Handle(PingMediatR request, RequestHandlerDelegate<int> next, CancellationToken cancellationToken)
{
return next(cancellationToken);
}
}
public sealed class LoggingBehaviorDispatchR : IPipelineBehavior<PingDispatchR, ValueTask<int>>
{
public required IRequestHandler<PingDispatchR, ValueTask<int>> NextPipeline { get; set; }
public ValueTask<int> Handle(PingDispatchR request, CancellationToken cancellationToken)
{
return NextPipeline.Handle(request, cancellationToken);
}
}
void, Task, or ValueTask as return types.Ideal for high-performance .NET applications.
public sealed class CounterStreamRequestMediatR : IStreamRequest<int> { }
TRequest to IStreamRequestpublic sealed class CounterStreamRequestDispatchR : IStreamRequest<PingDispatchR, ValueTask<int>> { }
public sealed class CounterStreamHandlerMediatR : IStreamRequestHandler<CounterStreamRequestMediatR, int>
{
public async IAsyncEnumerable<int> Handle(CounterStreamRequestMediatR request, CancellationToken cancellationToken)
{
yield return 1;
}
}
public sealed class CounterStreamHandlerDispatchR : IStreamRequestHandler<CounterStreamHandlerDispatchR, int>
{
public async IAsyncEnumerable<int> Handle(CounterStreamHandlerDispatchR request, CancellationToken cancellationToken)
{
yield return 1;
}
}
public sealed class CounterPipelineStreamHandler : IStreamPipelineBehavior<CounterStreamRequestMediatR, string>
{
public async IAsyncEnumerable<string> Handle(CounterStreamRequestMediatR request, StreamHandlerDelegate<string> next, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var response in next().WithCancellation(cancellationToken).ConfigureAwait(false))
{
yield return response;
}
}
}
public sealed class CounterPipelineStreamHandler : IStreamPipelineBehavior<CounterStreamRequestDispatchR, string>
{
public required IStreamRequestHandler<CounterStreamRequestDispatchR, string> NextPipeline { get; set; }
public async IAsyncEnumerable<string> Handle(CounterStreamRequestDispatchR request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var response in NextPipeline.Handle(request, cancellationToken).ConfigureAwait(false))
{
yield return response;
}
}
}
public sealed record Event(Guid Id) : INotification;
public sealed class EventHandler(ILogger<Event> logger) : INotificationHandler<Event>
{
public Task Handle(Event notification, CancellationToken cancellationToken)
{
logger.LogInformation("Received notification");
return Task.CompletedTask;
}
}
public sealed record Event(Guid Id) : INotification;
public sealed class EventHandler(ILogger<Event> logger) : INotificationHandler<Event>
{
public ValueTask Handle(Event notification, CancellationToken cancellationToken)
{
logger.LogInformation("Received notification");
return ValueTask.CompletedTask;
}
}
Send Method?public TResponse Send<TRequest, TResponse>(IRequest<TRequest, TResponse> request,
CancellationToken cancellationToken) where TRequest : class, IRequest, new()
{
return serviceProvider
.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
}
CreateStream Method?public IAsyncEnumerable<TResponse> CreateStream<TRequest, TResponse>(IStreamRequest<TRequest, TResponse> request,
CancellationToken cancellationToken) where TRequest : class, IStreamRequest, new()
{
return serviceProvider.GetRequiredService<IStreamRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
}
Only the handler is resolved and directly invoked!
Publish Method?public async ValueTask Publish<TNotification>(TNotification request, CancellationToken cancellationToken) where TNotification : INotification
{
var notificationsInDi = serviceProvider.GetRequiredService<IEnumerable<INotificationHandler<TNotification>>>();
var notifications = Unsafe.As<INotificationHandler<TNotification>[]>(notificationsInDi);
foreach (var notification in notifications)
{
var valueTask = notification.Handle(request, cancellationToken);
if (valueTask.IsCompletedSuccessfully is false) // <-- Handle sync notifications
{
await valueTask;
}
}
}
But the real magic happens behind the scenes when DI resolves the handler dependency:
💡 Tips:
We cache the handler using DI, so in scoped scenarios, the object is constructed only once and reused afterward.
In terms of Dependency Injection (DI), everything in Requests is an IRequestHandler, it's just the keys that differ. When you request a specific key, a set of 1+N objects is returned: the first one is the actual handler, and the rest are the pipeline behaviors.
services.AddScoped(handlerInterface, sp =>
{
var pipelinesWithHandler = Unsafe
.As<IRequestHandler[]>(sp.GetKeyedServices<IRequestHandler>(key));
IRequestHandler lastPipeline = pipelinesWithHandler[0];
for (int i = 1; i < pipelinesWithHandler.Length; i++)
{
var pipeline = pipelinesWithHandler[i];
pipeline.SetNext(lastPipeline);
lastPipeline = pipeline;
}
return lastPipeline;
});
This elegant design chains pipeline behaviors at resolution time — no static lists, no reflection, no magic.
It's simple! Just use the following code:
builder.Services.AddDispatchR(typeof(MyCommand).Assembly, withPipelines: true, withNotifications: true);
This code will automatically register all pipelines by default. If you need to register them in a specific order, you can either add them manually or write your own reflection logic:
builder.Services.AddDispatchR(typeof(MyCommand).Assembly, withPipelines: false, withNotifications: false);
builder.Services.AddScoped<IPipelineBehavior<MyCommand, int>, PipelineBehavior>();
builder.Services.AddScoped<IPipelineBehavior<MyCommand, int>, ValidationBehavior>();
builder.Services.AddScoped<IStreamPipelineBehavior<MyStreamCommand, int>, ValidationBehavior>();
builder.Services.AddScoped<INotificationHandler<Event>, EventHandler>();
dotnet add package DispatchR.Mediator --version 1.2.0
[!IMPORTANT] This benchmark was conducted using MediatR version 12.5.0 and the stable release of Mediator Source Generator, version 2.1.7. Version 3 of Mediator Source Generator was excluded due to significantly lower performance.





We welcome contributions to make this package even better! ❤️
Let's build something amazing together! 🚀