Blazingly fast CQRS command dispatching for .NET 10
Zero allocations in hot paths • Native AOT ready • Zero external dependencies
SharpDispatch is a lightweight, high-performance CQRS command dispatching library built on modern .NET 10 principles:
- 🚀 Sub-microsecond dispatch latency — Singleton handlers dispatch in ~10–15ns with zero allocations
- 🎯 Native AOT ready — Full support for ahead-of-time compilation via
CommandDispatcherBuilder - 📦 Zero dependencies — Relies only on
Microsoft.Extensions.DependencyInjection.Abstractions - ♻️ Zero-copy friendly — Pass
ReadOnlySpan<T>andMemory<T>through commands without allocation - 🔒 Type-safe — Compile-time checked handler registration with exhaustive dispatch
- 🧪 Test-friendly — In-memory dispatcher for fast, isolated unit tests
- 🌍 Standalone — No coupling to event sourcing, databases, or messaging — works everywhere
/// Marker interface for commandspublicinterfaceICommand{}/// Handles a specific command typepublicinterfaceICommandHandler<inTCommand>whereTCommand:ICommand{Task<CommandDispatchResult>HandleAsync(TCommandcommand,CancellationTokenct);}/// Dispatches commands to registered handlerspublicinterfaceICommandDispatcher{Task<CommandDispatchResult>DispatchAsync<TCommand>(TCommandcommand,CancellationTokencancellationToken=default)whereTCommand:ICommand;}/// Result of command dispatch (record struct — stack allocated)publicreadonlyrecordstructCommandDispatchResult(boolSuccess,string?Message){publicstaticCommandDispatchResultOk(string?message=null)=>new(true,message);publicstaticCommandDispatchResultFail(stringmessage)=>new(false,message);}dotnet add package SharpDispatchusingSharpDispatch;// CommandpublicclassCreateOrderCommand:ICommand{publicrequiredstringOrderId{get;init;}publicrequireddecimalAmount{get;init;}}// HandlerpublicclassCreateOrderCommandHandler:ICommandHandler<CreateOrderCommand>{privatereadonlyIOrderRepository_orderRepository;publicCreateOrderCommandHandler(IOrderRepositoryorderRepository){_orderRepository=orderRepository;}publicasyncTask<CommandDispatchResult>HandleAsync(CreateOrderCommandcommand,CancellationTokencancellationToken){varorder=newOrder{Id=command.OrderId,Amount=command.Amount};await_orderRepository.SaveAsync(order,cancellationToken);returnCommandDispatchResult.Ok($"Order {command.OrderId} created");}}usingMicrosoft.Extensions.DependencyInjection;varservices=newServiceCollection();// Register handlersservices.AddCommandHandler<CreateOrderCommand,CreateOrderCommandHandler>();// Choose your dispatcher:// Option A: Simple DI-based dispatcher (best for most apps)services.AddCommandDispatcher();// Option B: High-throughput optimized dispatcher (best for APIs)// services.AddOptimizedCommandDispatcher(cfg =>// {// cfg.AddHandler<CreateOrderCommand, CreateOrderCommandHandler>();// });varprovider=services.BuildServiceProvider();vardispatcher=provider.GetRequiredService<ICommandDispatcher>();// Dispatch!varresult=awaitdispatcher.DispatchAsync(newCreateOrderCommand{OrderId="ORD-001",Amount=99.99m});Console.WriteLine(result.Message);Resolves handlers from DI on each call. Best for:
- Most applications
- Mixed handler lifetimes (singleton, scoped, transient)
- Simple setup
services.AddCommandDispatcher();Overhead: ~50–200ns per dispatch (includes DI resolution)
Pre-builds typed delegates and uses FrozenDictionary for zero-allocation dispatch. Best for:
- APIs handling thousands of commands/sec
- Singleton handlers
- Latency-sensitive workloads
services.AddOptimizedCommandDispatcher(cfg =>{cfg.AddHandler<CreateOrderCommand,CreateOrderCommandHandler>();cfg.AddHandler<ShipOrderCommand,ShipOrderCommandHandler>();});Hot path: ~10–15ns for singleton handlers (FrozenDictionary lookup + delegate invoke)
Constructor: O(n handlers) — runs once at startup
In-memory handler registry without DI. Best for:
- Unit tests
- Scenarios where DI is unavailable
- Fast test iteration
vardispatcher=newInMemoryCommandDispatcher();dispatcher.RegisterHandler(newCreateOrderCommandHandler(mockRepo));varresult=awaitdispatcher.DispatchAsync(command);All APIs are fully async with cancellation token support:
// Respects CancellationTokenawaitdispatcher.DispatchAsync(command,cancellationToken);// Safe shutdownusingvarcts=newCancellationTokenSource(timeout:TimeSpan.FromSeconds(10));awaitdispatcher.DispatchAsync(command,cts.Token);CommandDispatcherBuilder enables zero-reflection, AOT-safe registration:
services.AddOptimizedCommandDispatcher(cfg =>{// No MakeGenericType, no Activator.CreateInstance — fully verifiable!cfg.AddHandler<CreateOrderCommand,CreateOrderCommandHandler>();cfg.AddHandler<ShipOrderCommand,ShipOrderCommandHandler>();});Publish as Native AOT:
dotnet publish -c Release --self-contained -r win-x64 \
-p:PublishAot=true -p:TrimMode=fullDispatch overhead (after handler execution):
| Scenario | Latency | Allocations |
|---|---|---|
| OptimizedCommandDispatcher (singleton) | ~10–15 ns | 0 |
| ServiceProviderCommandDispatcher | ~50–200 ns | 1–2 |
| InMemoryCommandDispatcher | ~20–40 ns | 0 |
Measured on modern Intel/AMD processors. Results vary by workload.
[Fact]publicasyncTaskCreateOrderCommand_WithValidData_ShouldSucceed(){// ArrangevarmockRepository=newMock<IOrderRepository>();varhandler=newCreateOrderCommandHandler(mockRepository.Object);vardispatcher=newInMemoryCommandDispatcher();dispatcher.RegisterHandler(handler);varcommand=newCreateOrderCommand{OrderId="ORD-001",Amount=99.99m};// Actvarresult=awaitdispatcher.DispatchAsync(command);// AssertAssert.True(result.Success);mockRepository.Verify(
x =>x.SaveAsync(It.IsAny<Order>(),It.IsAny<CancellationToken>()),Times.Once);}Handlers with scoped lifetime are resolved per dispatch:
services.AddOptimizedCommandDispatcher(cfg =>{cfg.AddHandler<ReportGenerationCommand,ReportGenerationHandler>(lifetime:ServiceLifetime.Scoped);});Use the base ICommandHandler<T> interface for complete control:
publicclassCustomOrderHandler:ICommandHandler<CreateOrderCommand>{privatereadonlyIServiceProvider_services;publicCustomOrderHandler(IServiceProviderservices)=>_services=services;publicasyncTask<CommandDispatchResult>HandleAsync(CreateOrderCommandcommand,CancellationTokencancellationToken){usingvarscope=_services.CreateScope();varrepository=scope.ServiceProvider.GetRequiredService<IOrderRepository>();// Custom scoping logicreturnCommandDispatchResult.Ok();}}Wrap dispatchers for logging, metrics, or middleware:
publicclassLoggingDispatcher:ICommandDispatcher{privatereadonlyICommandDispatcher_inner;privatereadonlyILogger<LoggingDispatcher>_logger;publicLoggingDispatcher(ICommandDispatcherinner,ILogger<LoggingDispatcher>logger){_inner=inner;_logger=logger;}publicasyncTask<CommandDispatchResult>DispatchAsync<TCommand>(TCommandcommand,CancellationTokencancellationToken=default)whereTCommand:ICommand{_logger.LogInformation("Dispatching {CommandType}",typeof(TCommand).Name);varresult=await_inner.DispatchAsync(command,cancellationToken);_logger.LogInformation("Dispatch result: {Success}",result.Success);returnresult;}}// Registerservices.AddCommandDispatcher();services.Decorate<ICommandDispatcher,LoggingDispatcher>();| Type | Purpose |
|---|---|
ICommand | Marker interface for commands |
ICommandHandler<TCommand> | Handler contract |
ICommandDispatcher | Dispatcher abstraction |
CommandDispatchResult | Stack-allocated result struct |
ServiceProviderCommandDispatcher | DI-based dispatcher |
InMemoryCommandDispatcher | In-memory test dispatcher |
OptimizedCommandDispatcher | High-performance dispatcher |
CommandDispatcherBuilder | AOT-safe fluent builder |
SharpDispatch is transport/database agnostic:
- ASP.NET Core: Use in controller actions or minimal APIs
- Hosted Services: Dispatch from background workers
- Message Queues: Serialize commands from Kafka/RabbitMQ and dispatch
- gRPC: Forward gRPC calls to command dispatch
- Event Sourcing: Dispatch commands that produce events (see
SharpCoreDB.CQRS)
namespaceMyApp.Api.Controllers;usingMicrosoft.AspNetCore.Mvc;usingSharpDispatch;[ApiController][Route("api/orders")]publicclassOrdersController(ICommandDispatcherdispatcher):ControllerBase{[HttpPost]publicasyncTask<IActionResult>CreateOrder([FromBody]CreateOrderCommandcommand){varresult=awaitdispatcher.DispatchAsync(command,HttpContext.RequestAborted);returnresult.Success?Ok(result):BadRequest(result.Message);}}- .NET 10 (requires C# 14)
Always enabled. Leverage #nullable enable:
publicclassOrderCommand:ICommand{publicrequiredstringOrderId{get;init;}// Non-null requiredpublicstring?Notes{get;init;}// Optional}Licensed under the MIT License. See LICENSE for details.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
All code must follow the project's C# 14 standards and performance guidelines.
- Open an issue for bug reports or feature requests
- Discussions are welcome for design questions
- Check existing documentation in the repository
Made with ❤️ for .NET developers who care about performance.
