DevSource.Dispatcher
1.0.2
See the version list below for details.
dotnet add package DevSource.Dispatcher --version 1.0.2
NuGet\Install-Package DevSource.Dispatcher -Version 1.0.2
<PackageReference Include="DevSource.Dispatcher" Version="1.0.2" />
<PackageVersion Include="DevSource.Dispatcher" Version="1.0.2" />
<PackageReference Include="DevSource.Dispatcher" />
paket add DevSource.Dispatcher --version 1.0.2
#r "nuget: DevSource.Dispatcher, 1.0.2"
#:package DevSource.Dispatcher@1.0.2
#addin nuget:?package=DevSource.Dispatcher&version=1.0.2
#tool nuget:?package=DevSource.Dispatcher&version=1.0.2
DevSource.Dispatcher
DevSource.Dispatcher is a high-performance .NET dispatcher built as an open-source alternative to MediatR.
It focuses on:
- Decoupled use-case execution with commands, queries, and notifications
- Deterministic and extensible pipelines for cross-cutting concerns
- Minimal runtime overhead with
ValueTask, delegate caching, and generated dispatch - Runtime independence from frameworks and mandatory DI containers
- A hybrid execution model: generated first, runtime fallback second
Overview
The solution is organized into three main projects:
src/DevSource.Dispatcher- public contracts only, with no external dependenciessrc/DevSource.Dispatcher.Engine- runtime engine, pipeline orchestration, caching, and DI integration helperssrc/DevSource.Dispatcher.SourceGenerator- compile-time dispatcher generation
Execution flow:
Request -> Dispatcher -> Pipeline -> Handler -> Response
Hybrid strategy:
- Try generated dispatch code
- Fallback to runtime resolution
Features
- Commands with and without responses
- Queries with single-handler execution
- Notifications with fan-out publishing
- Optional pipeline behaviors with deterministic ordering
CancellationTokensupportValueTask-based APIs- Delegate caching per request type
- Source-generator integration for generated dispatch paths
- Works with or without direct
IServiceProviderusage
Installation
The recommended installation experience is a single package:
dotnet add package DevSource.Dispatcher
The DevSource.Dispatcher package is the public entry point and is intended to deliver the full experience in one install:
- public contracts
- runtime engine
- source generator analyzer
That means consumers do not need to install DevSource.Dispatcher.Engine or DevSource.Dispatcher.SourceGenerator separately when using the NuGet package.
After installing the package, configure your handlers and register the runtime with:
using DevSource.Dispatcher.Engine;
using DevSource.Dispatcher.Generated;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<ICommandHandler<CreateOrderCommand, Guid>, CreateOrderHandler>();
services.AddTransient<IQueryHandler<GetOrderQuery, OrderDto>, GetOrderHandler>();
services.AddTransient<INotificationHandler<OrderCreatedNotification>, OrderCreatedHandler>();
services.AddDispatcher<GeneratedDispatcher>();
Quick Start
Register the runtime engine
using DevSource.Dispatcher.Engine;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<ICommandHandler<CreateOrderCommand, Guid>, CreateOrderHandler>();
services.AddTransient<IQueryHandler<GetOrderQuery, OrderDto>, GetOrderHandler>();
services.AddTransient<INotificationHandler<OrderCreatedNotification>, OrderCreatedHandler>();
services.AddDispatcher();
Register the generated dispatcher path
When the package is installed with analyzer support enabled, a GeneratedDispatcher type is emitted in DevSource.Dispatcher.Generated.
using DevSource.Dispatcher.Engine;
using DevSource.Dispatcher.Generated;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<ICommandHandler<CreateOrderCommand, Guid>, CreateOrderHandler>();
services.AddTransient<IQueryHandler<GetOrderQuery, OrderDto>, GetOrderHandler>();
services.AddDispatcher<GeneratedDispatcher>();
Use without DI
You can also use the dispatcher without IServiceProvider. In this mode, you provide your own resolver.
using DevSource.Dispatcher.Commands;
using DevSource.Dispatcher.Engine;
using DevSource.Dispatcher.Notifications;
using DevSource.Dispatcher.Queries;
var resolver = new ManualResolver(
new CreateOrderHandler(new InMemoryOrderRepository()),
new GetOrderHandler(new InMemoryOrderRepository()));
var commandDispatcher = new CommandDispatcher(resolver);
var queryDispatcher = new QueryDispatcher(resolver);
var notificationDispatcher = new NotificationDispatcher(resolver);
var mediator = new DevSource.Dispatcher.Engine.Mediator(commandDispatcher, queryDispatcher, notificationDispatcher);
var orderId = await mediator.SendAsync<CreateOrderCommand, Guid>(new CreateOrderCommand("Ada Lovelace"));
sealed class ManualResolver : IRequestHandlerResolver
{
private readonly ICommandHandler<CreateOrderCommand, Guid> _commandHandler;
private readonly IQueryHandler<GetOrderQuery, OrderDto> _queryHandler;
public ManualResolver(
ICommandHandler<CreateOrderCommand, Guid> commandHandler,
IQueryHandler<GetOrderQuery, OrderDto> queryHandler)
{
_commandHandler = commandHandler;
_queryHandler = queryHandler;
}
public ICommandHandler<TCommand> GetRequiredCommandHandler<TCommand>() where TCommand : ICommand
=> throw new NotSupportedException();
public ICommandHandler<TCommand, TResponse> GetRequiredCommandHandler<TCommand, TResponse>() where TCommand : ICommand<TResponse>
=> typeof(TCommand) == typeof(CreateOrderCommand) && typeof(TResponse) == typeof(Guid)
? (ICommandHandler<TCommand, TResponse>)_commandHandler
: throw new InvalidOperationException($"No handler for {typeof(TCommand).Name}.");
public IQueryHandler<TQuery, TResponse> GetRequiredQueryHandler<TQuery, TResponse>() where TQuery : IQuery<TResponse>
=> typeof(TQuery) == typeof(GetOrderQuery) && typeof(TResponse) == typeof(OrderDto)
? (IQueryHandler<TQuery, TResponse>)_queryHandler
: throw new InvalidOperationException($"No handler for {typeof(TQuery).Name}.");
public IEnumerable<IPipelineBehavior<TCommand>> GetCommandBehaviors<TCommand>() where TCommand : ICommand => [];
public IEnumerable<IPipelineBehavior<TRequest, TResponse>> GetBehaviors<TRequest, TResponse>() where TRequest : notnull => [];
public IEnumerable<INotificationHandler<TNotification>> GetNotificationHandlers<TNotification>() where TNotification : INotification => [];
}
Layered Sample
The repository includes a real layered sample under samples/:
samples/Order.Domain- domain model and repository abstractionsamples/Order.Application- commands, queries, notifications, handlers, pipeline behavior, and dispatcher registrationsamples/Order.Api- Minimal API host showing how to wire everything together
Run the sample API with:
dotnet run --project samples/Order.Api/Order.Api.csproj
Example requests:
curl -X POST http://localhost:5000/orders \
-H "Content-Type: application/json" \
-d "{\"customerName\":\"Ada Lovelace\",\"items\":[{\"productName\":\"Keyboard\",\"quantity\":1,\"unitPrice\":120.00}]}"
curl http://localhost:5000/orders/{orderId}
Usage Examples
Command with response
using DevSource.Dispatcher.Commands;
public sealed record CreateOrderCommand(string CustomerName) : ICommand<Guid>;
public sealed class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid>
{
public ValueTask<Guid> HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(Guid.NewGuid());
}
Dispatch it through IMediator:
using DevSource.Dispatcher;
var mediator = serviceProvider.GetRequiredService<IMediator>();
var orderId = await mediator.SendAsync<CreateOrderCommand, Guid>(new CreateOrderCommand("Ada Lovelace"));
Query
using DevSource.Dispatcher.Queries;
public sealed record GetOrderQuery(Guid OrderId) : IQuery<OrderDto>;
public sealed record OrderDto(Guid Id, string CustomerName);
public sealed class GetOrderHandler : IQueryHandler<GetOrderQuery, OrderDto>
{
public ValueTask<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(new OrderDto(query.OrderId, "Ada Lovelace"));
}
Dispatch it:
var order = await mediator.QueryAsync<GetOrderQuery, OrderDto>(new GetOrderQuery(orderId));
Notification
using DevSource.Dispatcher.Notifications;
public sealed record OrderCreatedNotification(Guid OrderId) : INotification;
public sealed class OrderCreatedHandler : INotificationHandler<OrderCreatedNotification>
{
public ValueTask HandleAsync(OrderCreatedNotification notification, CancellationToken cancellationToken = default)
=> ValueTask.CompletedTask;
}
Publish it:
await mediator.PublishAsync(new OrderCreatedNotification(orderId));
Pipeline behavior
using DevSource.Dispatcher;
using DevSource.Dispatcher.Engine;
public sealed class LoggingBehavior : IPipelineBehavior<CreateOrderCommand, Guid>, IOrderedPipelineBehavior
{
public int Order => 100;
public async ValueTask<Guid> HandleAsync(
CreateOrderCommand request,
RequestHandlerDelegate<Guid> next,
CancellationToken cancellationToken)
{
Console.WriteLine($"Creating order for {request.CustomerName}");
var response = await next().ConfigureAwait(false);
Console.WriteLine($"Created order {response}");
return response;
}
}
The engine executes ordered behaviors first by Order, then by type name as a deterministic fallback.
Running Tests
Run the unit test suite with:
dotnet test tests/DevSource.Dispatcher.Tests/DevSource.Dispatcher.Tests.csproj
To validate the sample application build as well:
dotnet build samples/Order.Api/Order.Api.csproj
The project currently uses:
- xUnit
- Moq
- Bogus
Running Benchmarks
Run the benchmark project with:
dotnet run --project tests/DevSource.Dispatcher.Benchmarks/DevSource.Dispatcher.Benchmarks.csproj -- --filter *DispatcherBenchmarks*
The benchmark compares DevSource.Dispatcher against:
- MediatR
- WolverineFx
Artifacts are written to:
tests/DevSource.Dispatcher.Benchmarks/BenchmarkDotNet.Artifacts/results/
Important note:
- current benchmark artifacts were collected from the
RELEASE-based run stored intests/DevSource.Dispatcher.Benchmarks/BenchmarkDotNet.Artifacts/results/DevSource.Dispatcher.Benchmarks.DispatcherBenchmarks-report-github.md
Latest benchmark snapshot:
| Method | Mean | Allocated |
|---|---|---|
| DevSource_Runtime_Command | 87.66 ns | 280 B |
| DevSource_Generated_Command | 97.16 ns | 280 B |
| Wolverine_Command | 190.59 ns | 656 B |
| MediatR_Command | 211.37 ns | 1376 B |
| DevSource_Runtime_Query | 85.94 ns | 280 B |
| DevSource_Generated_Query | 100.80 ns | 280 B |
| Wolverine_Query | 192.75 ns | 656 B |
| MediatR_Query | 266.44 ns | 1440 B |
In the current published benchmark artifacts, DevSource.Dispatcher is faster and allocates less memory than both MediatR and WolverineFx for the measured command and query scenarios.
Roadmap
Potential next extensions include:
- Validation extensions
- Logging extensions
- Resilience extensions
- Observability extensions
- Streaming support
- Additional integration adapters
Contributing
Contributions, issues, and design discussions are welcome.
If you want to contribute:
- Fork the repository
- Create a feature branch
- Add or update tests
- Run the test suite and benchmarks when relevant
- Open a pull request
Contributors
Current contributors:
Repository Structure
src/
DevSource.Dispatcher/
DevSource.Dispatcher.Engine/
DevSource.Dispatcher.SourceGenerator/
samples/
Order.Api/
Order.Application/
Order.Domain/
tests/
DevSource.Dispatcher.Tests/
DevSource.Dispatcher.Benchmarks/
DevSource.slnx
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.