** 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.
- 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>- Open-generic
INotificationHandler<TNotification> where TNotification : INotification
- Simple Request:
💡 Tip:If you're looking for a mediator with the raw performance of hand-written code, DispatchR is built for you.
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
In the following, you will see the key differences and implementation details between MediatR and DispatchR.
publicsealedclassPingMediatR:IRequest<int>{}- Sending
TRequesttoIRequest - Precise selection of output for both
asyncandsynchandlers- Ability to choose between
TaskandValueTask
- Ability to choose between
publicsealedclassPingDispatchR:IRequest<PingDispatchR,ValueTask<int>>{}Important
Always use a generic return type such as Task<TResult> or ValueTask<TResult>. Using a bare Task or ValueTask (without a type argument) may prevent the handler from being triggered when pipeline behaviors or validators are present.
publicsealedclassPingHandlerMediatR:IRequestHandler<PingMediatR,int>{publicTask<int>Handle(PingMediatRrequest,CancellationTokencancellationToken){returnTask.FromResult(0);}}publicsealedclassPingHandlerDispatchR:IRequestHandler<PingDispatchR,ValueTask<int>>{publicValueTask<int>Handle(PingDispatchRrequest,CancellationTokencancellationToken){returnValueTask.FromResult(0);}}publicsealedclassLoggingBehaviorMediat:IPipelineBehavior<PingMediatR,int>{publicTask<int>Handle(PingMediatRrequest,RequestHandlerDelegate<int>next,CancellationTokencancellationToken){returnnext(cancellationToken);}}- Use Chain of Responsibility pattern
publicsealedclassLoggingBehaviorDispatchR:IPipelineBehavior<PingDispatchR,ValueTask<int>>{publicrequiredIRequestHandler<PingDispatchR,ValueTask<int>>NextPipeline{get;set;}publicValueTask<int>Handle(PingDispatchRrequest,CancellationTokencancellationToken){returnNextPipeline.Handle(request,cancellationToken);}}- 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.
publicclassGenericPipelineBehavior<TRequest,TResponse>():IPipelineBehavior<TRequest,ValueTask<TResponse>>whereTRequest:class,IRequest<TRequest,ValueTask<TResponse>>{publicrequiredIRequestHandler<TRequest,ValueTask<TResponse>>NextPipeline{get;set;}publicValueTask<TResponse>Handle(TRequestrequest,CancellationTokencancellationToken){// You can add custom logic here, like logging or validation// This pipeline behavior can be used for any request typereturnNextPipeline.Handle(request,cancellationToken);}}- 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.
publicsealedclassCounterStreamRequestMediatR:IStreamRequest<int>{}- Sending
TRequesttoIStreamRequest
publicsealedclassCounterStreamRequestDispatchR:IStreamRequest<CounterStreamRequestDispatchR,int>{}publicsealedclassCounterStreamHandlerMediatR:IStreamRequestHandler<CounterStreamRequestMediatR,int>{publicasyncIAsyncEnumerable<int>Handle(CounterStreamRequestMediatRrequest,CancellationTokencancellationToken){yieldreturn1;}}publicsealedclassCounterStreamHandlerDispatchR:IStreamRequestHandler<CounterStreamRequestDispatchR,int>{publicasyncIAsyncEnumerable<int>Handle(CounterStreamRequestDispatchRrequest,CancellationTokencancellationToken){yieldreturn1;}}publicsealedclassCounterPipelineStreamHandler:IStreamPipelineBehavior<CounterStreamRequestMediatR,string>{publicasyncIAsyncEnumerable<string>Handle(CounterStreamRequestMediatRrequest,StreamHandlerDelegate<string>next,[EnumeratorCancellation]CancellationTokencancellationToken){awaitforeach(varresponseinnext().WithCancellation(cancellationToken).ConfigureAwait(false)){yieldreturnresponse;}}}- Use Chain of Responsibility pattern
publicsealedclassCounterPipelineStreamHandler:IStreamPipelineBehavior<CounterStreamRequestDispatchR,string>{publicrequiredIStreamRequestHandler<CounterStreamRequestDispatchR,string>NextPipeline{get;set;}publicasyncIAsyncEnumerable<string>Handle(CounterStreamRequestDispatchRrequest,[EnumeratorCancellation]CancellationTokencancellationToken){awaitforeach(varresponseinNextPipeline.Handle(request,cancellationToken).ConfigureAwait(false)){yieldreturnresponse;}}}publicclassGenericStreamPipelineBehavior<TRequest,TResponse>():IStreamPipelineBehavior<TRequest,TResponse>whereTRequest:class,IStreamRequest<TRequest,TResponse>{publicIStreamRequestHandler<TRequest,TResponse>NextPipeline{get;set;}publicasyncIAsyncEnumerable<TResponse>Handle(TRequestrequest,CancellationTokencancellationToken){awaitforeach(varresponseinNextPipeline.Handle(request,cancellationToken).ConfigureAwait(false)){yieldreturnresponse;}}}publicsealedrecordEvent(GuidId):INotification;publicsealedclassEventHandler(ILogger<Event>logger):INotificationHandler<Event>{publicTaskHandle(Eventnotification,CancellationTokencancellationToken){logger.LogInformation("Received notification");returnTask.CompletedTask;}}- Use ValueTask
publicsealedrecordEvent(GuidId):INotification;publicsealedclassEventHandler(ILogger<Event>logger):INotificationHandler<Event>{publicValueTaskHandle(Eventnotification,CancellationTokencancellationToken){logger.LogInformation("Received notification");returnValueTask.CompletedTask;}}- A single open-generic handler receives every notification type — ideal for cross-cutting concerns such as logging, auditing, or telemetry.
publicsealedclassAllNotificationsLogger<TNotification>(ILogger<AllNotificationsLogger<TNotification>>logger):INotificationHandler<TNotification>whereTNotification:INotification{publicValueTaskHandle(TNotificationnotification,CancellationTokencancellationToken){logger.LogInformation("[Generic] Received notification of type {NotificationType}: {@Notification}",typeof(TNotification).Name,notification);returnValueTask.CompletedTask;}}DispatchR is designed with one goal in mind: maximize performance with minimal memory usage. Here's how it accomplishes that:
publicTResponseSend<TRequest,TResponse>(IRequest<TRequest,TResponse>request,CancellationTokencancellationToken)whereTRequest:class,IRequest{returnserviceProvider.GetRequiredService<IRequestHandler<TRequest,TResponse>>().Handle(Unsafe.As<TRequest>(request),cancellationToken);}publicIAsyncEnumerable<TResponse>CreateStream<TRequest,TResponse>(IStreamRequest<TRequest,TResponse>request,CancellationTokencancellationToken)whereTRequest:class,IStreamRequest{returnserviceProvider.GetRequiredService<IStreamRequestHandler<TRequest,TResponse>>().Handle(Unsafe.As<TRequest>(request),cancellationToken);}Only the handler is resolved and directly invoked!
publicasyncValueTaskPublish<TNotification>(TNotificationrequest,CancellationTokencancellationToken)whereTNotification:INotification{varnotificationsInDi=serviceProvider.GetRequiredService<IEnumerable<INotificationHandler<TNotification>>>();varnotifications=Unsafe.As<INotificationHandler<TNotification>[]>(notificationsInDi);foreach(varnotificationinnotifications){varvalueTask=notification.Handle(request,cancellationToken);if(valueTask.IsCompletedSuccessfullyisfalse)// <-- Handle sync notifications{awaitvalueTask;}}}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 =>{varpipelinesWithHandler=Unsafe.As<IRequestHandler[]>(sp.GetKeyedServices<IRequestHandler>(key));IRequestHandlerlastPipeline=pipelinesWithHandler[0];for(inti=1;i<pipelinesWithHandler.Length;i++){varpipeline=pipelinesWithHandler[i];pipeline.SetNext(lastPipeline);lastPipeline=pipeline;}returnlastPipeline;});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 pass the order via ConfigurationOptions, as shown in the example below.
Additionally, you can include or exclude specific handlers from an assembly — which is especially useful when working with Aspire.
You can also check the Samples section to see the Aspire-specific example.
builder.Services.AddDispatchR(options =>{options.Assemblies.Add(typeof(DispatchRSample.Ping).Assembly);options.RegisterPipelines=true;options.RegisterNotifications=true;options.PipelineOrder=[typeof(DispatchRSample.FirstPipelineBehavior),typeof(DispatchRSample.SecondPipelineBehavior),typeof(DispatchRSample.GenericPipelineBehavior<,>)];options.IncludeHandlers=null;options.ExcludeHandlers=null;});If you need additional customization, 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>();- Automatic pipeline and notification registration is enabled by default
- Manual registration allows for custom pipeline or notification ordering
- You can implement custom reflection if needed
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! ❤️
- Found a bug? → Open an issue
- Have an idea? → Suggest a feature
- Want to code? → Submit a PR
Let's build something amazing together! 🚀




