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 commands
public interface ICommand { }
/// Handles a specific command type
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
Task<CommandDispatchResult> HandleAsync(TCommand command, CancellationToken ct);
}
/// Dispatches commands to registered handlers
public interface ICommandDispatcher
{
Task<CommandDispatchResult> DispatchAsync<TCommand>(
TCommand command,
CancellationToken cancellationToken = default)
where TCommand : ICommand;
}
/// Result of command dispatch (record struct โ stack allocated)
public readonly record struct CommandDispatchResult(bool Success, string? Message)
{
public static CommandDispatchResult Ok(string? message = null) => new(true, message);
public static CommandDispatchResult Fail(string message) => new(false, message);
}dotnet add package SharpDispatchusing SharpDispatch;
// Command
public class CreateOrderCommand : ICommand
{
public required string OrderId { get; init; }
public required decimal Amount { get; init; }
}
// Handler
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
private readonly IOrderRepository _orderRepository;
public CreateOrderCommandHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<CommandDispatchResult> HandleAsync(
CreateOrderCommand command,
CancellationToken cancellationToken)
{
var order = new Order { Id = command.OrderId, Amount = command.Amount };
await _orderRepository.SaveAsync(order, cancellationToken);
return CommandDispatchResult.Ok($"Order {command.OrderId} created");
}
}using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Register handlers
services.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>();
// });
var provider = services.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<ICommandDispatcher>();
// Dispatch!
var result = await dispatcher.DispatchAsync(
new CreateOrderCommand { 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
var dispatcher = new InMemoryCommandDispatcher();
dispatcher.RegisterHandler(new CreateOrderCommandHandler(mockRepo));
var result = await dispatcher.DispatchAsync(command);All APIs are fully async with cancellation token support:
// Respects CancellationToken
await dispatcher.DispatchAsync(command, cancellationToken);
// Safe shutdown
using var cts = new CancellationTokenSource(timeout: TimeSpan.FromSeconds(10));
await dispatcher.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]
public async Task CreateOrderCommand_WithValidData_ShouldSucceed()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var handler = new CreateOrderCommandHandler(mockRepository.Object);
var dispatcher = new InMemoryCommandDispatcher();
dispatcher.RegisterHandler(handler);
var command = new CreateOrderCommand { OrderId = "ORD-001", Amount = 99.99m };
// Act
var result = await dispatcher.DispatchAsync(command);
// Assert
Assert.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:
public class CustomOrderHandler : ICommandHandler<CreateOrderCommand>
{
private readonly IServiceProvider _services;
public CustomOrderHandler(IServiceProvider services) => _services = services;
public async Task<CommandDispatchResult> HandleAsync(
CreateOrderCommand command,
CancellationToken cancellationToken)
{
using var scope = _services.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
// Custom scoping logic
return CommandDispatchResult.Ok();
}
}Wrap dispatchers for logging, metrics, or middleware:
public class LoggingDispatcher : ICommandDispatcher
{
private readonly ICommandDispatcher _inner;
private readonly ILogger<LoggingDispatcher> _logger;
public LoggingDispatcher(ICommandDispatcher inner, ILogger<LoggingDispatcher> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<CommandDispatchResult> DispatchAsync<TCommand>(
TCommand command,
CancellationToken cancellationToken = default)
where TCommand : ICommand
{
_logger.LogInformation("Dispatching {CommandType}", typeof(TCommand).Name);
var result = await _inner.DispatchAsync(command, cancellationToken);
_logger.LogInformation("Dispatch result: {Success}", result.Success);
return result;
}
}
// Register
services.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)
namespace MyApp.Api.Controllers;
using Microsoft.AspNetCore.Mvc;
using SharpDispatch;
[ApiController]
[Route("api/orders")]
public class OrdersController(ICommandDispatcher dispatcher) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)
{
var result = await dispatcher.DispatchAsync(command, HttpContext.RequestAborted);
return result.Success
? Ok(result)
: BadRequest(result.Message);
}
}- .NET 10 (requires C# 14)
Always enabled. Leverage #nullable enable:
public class OrderCommand : ICommand
{
public required string OrderId { get; init; } // Non-null required
public string? 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.
