Skip to content

Latest commit

History

History
441 lines (320 loc) · 12 KB

File metadata and controls

441 lines (320 loc) · 12 KB
SharpDispatch Logo

SharpDispatch

NuGet VersionLicense: MIT.NET 10Native AOT

Blazingly fast CQRS command dispatching for .NET 10
Zero allocations in hot paths • Native AOT ready • Zero external dependencies

⚡ Why SharpDispatch?

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> and Memory<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

📖 Core Abstractions

/// 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);}

🚀 Quick Start

1. Install

dotnet add package SharpDispatch

2. Define Commands & Handlers

usingSharpDispatch;// 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");}}

3. Register & Dispatch

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);

🎯 Three Dispatcher Implementations

ServiceProviderCommandDispatcher (Default)

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)


OptimizedCommandDispatcher (Recommended for High Throughput)

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


InMemoryCommandDispatcher (For Testing)

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);

🌍 Async-First Design

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);

🔒 Native AOT Ready

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=full

📊 Performance

Dispatch overhead (after handler execution):

ScenarioLatencyAllocations
OptimizedCommandDispatcher (singleton)~10–15 ns0
ServiceProviderCommandDispatcher~50–200 ns1–2
InMemoryCommandDispatcher~20–40 ns0

Measured on modern Intel/AMD processors. Results vary by workload.


🧪 Testing Example

[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);}

🏗️ Advanced Patterns

Scoped Handlers

Handlers with scoped lifetime are resolved per dispatch:

services.AddOptimizedCommandDispatcher(cfg =>{cfg.AddHandler<ReportGenerationCommand,ReportGenerationHandler>(lifetime:ServiceLifetime.Scoped);});

Custom Handler Factories

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();}}

Chained Dispatchers

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>();

📦 What's Inside

TypePurpose
ICommandMarker interface for commands
ICommandHandler<TCommand>Handler contract
ICommandDispatcherDispatcher abstraction
CommandDispatchResultStack-allocated result struct
ServiceProviderCommandDispatcherDI-based dispatcher
InMemoryCommandDispatcherIn-memory test dispatcher
OptimizedCommandDispatcherHigh-performance dispatcher
CommandDispatcherBuilderAOT-safe fluent builder

🤝 Integration with Other Libraries

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)

📚 Example: ASP.NET Core Integration

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);}}

⚙️ Configuration

Target Frameworks

  • .NET 10 (requires C# 14)

Nullable Reference Types

Always enabled. Leverage #nullable enable:

publicclassOrderCommand:ICommand{publicrequiredstringOrderId{get;init;}// Non-null requiredpublicstring?Notes{get;init;}// Optional}

📄 License

Licensed under the MIT License. See LICENSE for details.


🙌 Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -am 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

All code must follow the project's C# 14 standards and performance guidelines.


📮 Questions?

  • Open an issue for bug reports or feature requests
  • Discussions are welcome for design questions
  • Check existing documentation in the repository

⭐ Star us on GitHub!

Made with ❤️ for .NET developers who care about performance.