DispatchR
Fast, zero-alloc alternative to MediatR for .NET – minimal, blazing fast, and DI-friendly.
Install / Use
/learn @hasanxdev/DispatchRREADME
<img src="https://github.com/hasanxdev/DispatchR/raw/main/icon-mini.png" width="26">ispatchR 🚀
A High-Performance Mediator Implementation for .NET :trollface:
** 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.
⚡ Key Features
- Built entirely on top of Dependency Injection
- Zero runtime reflection after registration
- Choose your handler return type:
Task,ValueTask, orSynchronous Method - Allocates nothing on the heap — ideal for high-throughput scenarios
- Outperforms existing solutions in most real-world benchmarks
- Seamlessly compatible with MediatR — migrate with minimal effort
- Include or exclude a set of handlers from an assembly — ideal for use with Aspire
- Currently supports
- Simple Request:
IRequest<TRquest, TResponse>IRequestHandler<TRequest, TResponse>IPipelineBehavior<TRequest, TResponse>
- Stream Request:
IStreamRequest<TRquest, TResponse>IStreamRequestHandler<TRequest, TResponse>IStreamPipelineBehavior<TRequest, TResponse>
- Notifications:
INotificationINotificationHandler<TRequestEvent>
- Simple Request:
:bulb: Tip: If you're looking for a mediator with the raw performance of hand-written code, DispatchR is built for you.
✨ How to install?
dotnet add package DispatchR.Mediator
You can also separately add only the abstractions, which include the interfaces, in another layer:
dotnet add package DispatchR.Mediator.Abstractions
Syntax Comparison: DispatchR vs MediatR
In the following, you will see the key differences and implementation details between MediatR and DispatchR.
Request Definition
MediatR
public sealed class PingMediatR : IRequest<int> { }
DispatchR
- Sending
TRequesttoIRequest - Precise selection of output for both
asyncandsynchandlers- Ability to choose between
TaskandValueTask
- Ability to choose between
public sealed class PingDispatchR : IRequest<PingDispatchR, ValueTask<int>> { }
Handler Definition
MediatR
public sealed class PingHandlerMediatR : IRequestHandler<PingMediatR, int>
{
public Task<int> Handle(PingMediatR request, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}
DispatchR (Don't change)
public sealed class PingHandlerDispatchR : IRequestHandler<PingDispatchR, ValueTask<int>>
{
public ValueTask<int> Handle(PingDispatchR request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(0);
}
}
Pipeline Behavior
MediatR
public sealed class LoggingBehaviorMediat : IPipelineBehavior<PingMediatR, int>
{
public Task<int> Handle(PingMediatR request, RequestHandlerDelegate<int> next, CancellationToken cancellationToken)
{
return next(cancellationToken);
}
}
DispatchR
- Use Chain of Responsibility pattern
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);
}
}
Generic pipeline behavior DispatchR
- For every kind of return type —
Task,ValueTask, or synchronous methods — you need to write a generic pipeline behavior. However, you don't need a separate pipeline for each request. As shown in the code below, this is a GenericPipeline for requests that return aValueTask.
public class GenericPipelineBehavior<TRequest, TResponse>() : IPipelineBehavior<TRequest, ValueTask<TResponse>>
where TRequest : class, IRequest<TRequest, ValueTask<TResponse>>
{
public required IRequestHandler<TRequest, ValueTask<TResponse>> NextPipeline { get; set; }
public ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
// You can add custom logic here, like logging or validation
// This pipeline behavior can be used for any request type
return NextPipeline.Handle(request, cancellationToken);
}
}
Summary
- DispatchR lets the request itself define the return type.
- No runtime reflection in DispatchR — it's optimized for performance.
- No static behavior chains — pipelines are chained via DI and handler wiring.
- Supports
void,Task, orValueTaskas return types.
Ideal for high-performance .NET applications.
Stream Request Definition
MediatR Stream
public sealed class CounterStreamRequestMediatR : IStreamRequest<int> { }
DispatchR
- Sending
TRequesttoIStreamRequest
public sealed class CounterStreamRequestDispatchR : IStreamRequest<PingDispatchR, ValueTask<int>> { }
Stream Handler Definition
Stream Handler MediatR
public sealed class CounterStreamHandlerMediatR : IStreamRequestHandler<CounterStreamRequestMediatR, int>
{
public async IAsyncEnumerable<int> Handle(CounterStreamRequestMediatR request, CancellationToken cancellationToken)
{
yield return 1;
}
}
Stream Handler DispatchR (Don't change)
public sealed class CounterStreamHandlerDispatchR : IStreamRequestHandler<CounterStreamHandlerDispatchR, int>
{
public async IAsyncEnumerable<int> Handle(CounterStreamHandlerDispatchR request, CancellationToken cancellationToken)
{
yield return 1;
}
}
Stream Pipeline Behavior
Stream Pipeline MediatR
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;
}
}
}
Stream Pipeline DispatchR
- Use Chain of Responsibility pattern
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;
}
}
}
Generic stream pipeline behavior DispatchR
public class GenericStreamPipelineBehavior<TRequest, TResponse>() : IStreamPipelineBehavior<TRequest, TResponse>
where TRequest : class, IStreamRequest<TRequest, TResponse>
{
public IStreamRequestHandler<TRequest, TResponse> NextPipeline { get; set; }
public async IAsyncEnumerable<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
await foreach (var response in NextPipeline.Handle(request, cancellationToken).ConfigureAwait(false))
{
yield return response;
}
}
}
Notification
Notification MediatR
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;
}
}
Stream Pipeline DispatchR
- Use ValueTask
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;
}
}
⚡ How DispatchR Achieves High Performance
DispatchR is designed with one goal in mind: maximize performance with minimal memory usage. Here's how it accomplishes that:
What Happens Inside the Send Method?
public TResponse Send<TRequest, TResponse>(IRequest<TRequest, TResponse> request,
CancellationToken cancellationToken) where TRequest : class, IRequest
{
return serviceProvider
.GetRequiredService<IRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
}
What Happens Inside the CreateStream Method?
public IAsyncEnumerable<TResponse> CreateStream<TRequest, TResponse>(IStreamRequest<TRequest, TResponse> request,
CancellationToken cancellationToken) where TRequest : class, IStreamRequest
{
return serviceProvider.GetRequiredService<IStreamRequestHandler<TRequest, TResponse>>()
.Handle(Unsafe.As<TRequest>(request), cancellationToken);
}
Only the handler is resolved and directly invoked!
What Happens Inside the Publish Method?
