Send0xx.Dispatcher.DependencyInjection.Extensions
1.0.0-preview.5
Send0xx.Dispatcher.DependencyInjection
Additional DetailsPackage was merged into the main DI package in preview.7
dotnet add package Send0xx.Dispatcher.DependencyInjection.Extensions --version 1.0.0-preview.5
NuGet\Install-Package Send0xx.Dispatcher.DependencyInjection.Extensions -Version 1.0.0-preview.5
<PackageReference Include="Send0xx.Dispatcher.DependencyInjection.Extensions" Version="1.0.0-preview.5" />
<PackageVersion Include="Send0xx.Dispatcher.DependencyInjection.Extensions" Version="1.0.0-preview.5" />
<PackageReference Include="Send0xx.Dispatcher.DependencyInjection.Extensions" />
paket add Send0xx.Dispatcher.DependencyInjection.Extensions --version 1.0.0-preview.5
#r "nuget: Send0xx.Dispatcher.DependencyInjection.Extensions, 1.0.0-preview.5"
#:package Send0xx.Dispatcher.DependencyInjection.Extensions@1.0.0-preview.5
#addin nuget:?package=Send0xx.Dispatcher.DependencyInjection.Extensions&version=1.0.0-preview.5&prerelease
#tool nuget:?package=Send0xx.Dispatcher.DependencyInjection.Extensions&version=1.0.0-preview.5&prerelease
Dispatcher
Dispatcher is a small CQRS library for .NET applications that use dependency injection. It provides focused APIs for queries, commands, notifications, handlers, and pipeline behaviors.
Dispatcher supports two registration modes:
- Reflection-based registration for a straightforward application setup.
- Source-generated registration and dispatch for trimming and Native AOT.
Dispatch itself does not use reflection. Handler routes are stored in frozen dictionaries, and handlers and pipeline behaviors are resolved from the current dependency-injection scope.
Install
For the simplest Microsoft dependency-injection setup, install:
dotnet add package Send0xx.Dispatcher.DependencyInjection --version 1.0.0-preview.5
For source-generated registration and Native AOT, install instead:
dotnet add package Send0xx.Dispatcher.SourceGeneration --version 1.0.0-preview.5
Choose one implementation package. Both bring in the abstractions, runtime, and Microsoft DI integration they require.
Quick start
Define a query and its handler:
using Dispatcher;
public sealed record GetGreetingQuery(string Name) : IQuery<string>;
internal sealed class GetGreetingQueryHandler
: IQueryHandler<GetGreetingQuery, string>
{
public ValueTask<string> HandleAsync(
GetGreetingQuery query,
CancellationToken cancellationToken = default) =>
ValueTask.FromResult($"Hello, {query.Name}!");
}
Register Dispatcher and scan the application assembly for handlers:
using Dispatcher.DependencyInjection;
builder.Services
.AddDispatcher()
.AddDispatcherHandlers(typeof(Program).Assembly);
AddDispatcher() registers infrastructure only and never scans assemblies implicitly.
AddDispatcherHandlers() registers internal handler classes. Registering the same assembly more than once is safe.
Handler scanning accepts the same options type when a different handler lifetime is required:
builder.Services.AddDispatcherHandlers(
typeof(Program).Assembly,
options => options.ServiceLifetime = ServiceLifetime.Singleton);
Typed registration methods use the same shape:
using Dispatcher.DependencyInjection.Extensions;
builder.Services.AddQueryHandler<GetGreetingQuery, string, GetGreetingQueryHandler>(options =>
options.ServiceLifetime = ServiceLifetime.Singleton);
Inject a focused dispatcher interface and send the query:
app.MapGet("/greetings/{name}", async (
string name,
IQueryDispatcher queries,
CancellationToken cancellationToken) =>
{
var greeting = await queries.QueryAsync(
new GetGreetingQuery(name),
cancellationToken);
return Results.Ok(greeting);
});
Dispatcher and handlers are scoped by default. Resolve them inside a DI scope, as ASP.NET Core does for each request.
The dispatcher itself can be registered as transient when an application needs a new instance for every resolution:
builder.Services.AddDispatcher(options =>
options.ServiceLifetime = ServiceLifetime.Transient);
DispatcherOptions is in the Dispatcher.DependencyInjection namespace. Dispatcher registration supports Scoped and Transient; Singleton is rejected because it would capture the root service provider and could not safely resolve scoped handlers or pipeline behaviors. Handler registration supports all three Microsoft DI lifetimes. Behavior lifetimes are configured independently through their registration methods.
Messages and handlers
Queries
A query always has a response:
public sealed record GetOrderQuery(Guid Id) : IQuery<Order?>;
internal sealed class GetOrderQueryHandler(OrderStore store)
: IQueryHandler<GetOrderQuery, Order?>
{
public ValueTask<Order?> HandleAsync(
GetOrderQuery query,
CancellationToken cancellationToken = default) =>
ValueTask.FromResult(store.Find(query.Id));
}
Dispatch it with QueryAsync:
var order = await queries.QueryAsync(new GetOrderQuery(id), cancellationToken);
Commands
A command may return a response:
public sealed record CreateOrderCommand(string ProductId, int Quantity)
: ICommand<Guid>;
internal sealed class CreateOrderCommandHandler
: ICommandHandler<CreateOrderCommand, Guid>
{
public ValueTask<Guid> HandleAsync(
CreateOrderCommand command,
CancellationToken cancellationToken = default) =>
ValueTask.FromResult(Guid.NewGuid());
}
Or it may be resultless:
public sealed record ClearOrdersCommand : ICommand;
internal sealed class ClearOrdersCommandHandler
: ICommandHandler<ClearOrdersCommand>
{
public ValueTask HandleAsync(
ClearOrdersCommand command,
CancellationToken cancellationToken = default) =>
ValueTask.CompletedTask;
}
Execute both forms with ExecuteAsync:
var orderId = await commands.ExecuteAsync(
new CreateOrderCommand("keyboard", 2),
cancellationToken);
await commands.ExecuteAsync(new ClearOrdersCommand(), cancellationToken);
Notifications
Notifications can have zero or more handlers:
public sealed record OrderCreated(Guid OrderId) : INotification;
internal sealed class RecordOrderCreated
: INotificationHandler<OrderCreated>
{
public ValueTask HandleAsync(
OrderCreated notification,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"Order {notification.OrderId} was created.");
return ValueTask.CompletedTask;
}
}
Publish a notification with PublishAsync:
await publisher.PublishAsync(new OrderCreated(orderId), cancellationToken);
Notification handlers run sequentially in registration order. Publishing a notification with no handlers is a no-op.
Queries and commands require exactly one handler. A missing handler throws HandlerNotFoundException, and duplicate handlers throw DuplicateHandlerException. Dispatch uses the exact concrete message type; polymorphic routing is not currently supported.
Pipeline behaviors
Pipeline behaviors apply cross-cutting work around queries and commands:
internal sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest
{
public async ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"Executing {typeof(TRequest).Name}");
return await next(cancellationToken);
}
}
Register an open generic behavior:
builder.Services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
The first registered behavior is the outermost. Behaviors may short-circuit by returning without calling next, and they may pass a replacement cancellation token to next.
The same IPipelineBehavior<TRequest, TResponse> contract handles queries and both command shapes. A resultless ICommand is adapted to Unit only inside the pipeline; its public handler and dispatch methods remain resultless.
Source generation and Native AOT
Send0xx.Dispatcher.SourceGeneration generates typed handler registrations and a dispatcher implementation. Reflection is not used for registration or dispatch.
In a single-project application, opt in at assembly level and give the generated extension methods unique names:
using Dispatcher;
[assembly: GenerateDispatcherHandlers("AddApplicationHandlers")]
[assembly: GenerateDispatcher("AddDispatcher")]
Register the generated dispatcher and handlers:
builder.Services
.AddDispatcher()
.AddApplicationHandlers();
The generated AddDispatcher method accepts the same lifetime options:
builder.Services
.AddDispatcher(options =>
options.ServiceLifetime = ServiceLifetime.Transient)
.AddApplicationHandlers(options =>
options.ServiceLifetime = ServiceLifetime.Singleton);
Dispatcher and generated handler lifetimes are configured independently; the example uses a transient dispatcher and singleton handlers.
Handlers may remain internal. The generator discovers queries, commands, notifications, and pipeline behaviors at compile time and emits explicit DI registrations and frozen dispatch tables.
For applications split across assemblies, each referenced assembly can generate its own handler-registration method while the host generates the dispatcher:
// In a referenced handlers assembly
[assembly: GenerateDispatcherHandlers("AddOrderHandlers")]
// In the host assembly
[assembly: GenerateDispatcher("AddDispatcher")]
builder.Services
.AddDispatcher()
.AddOrderHandlers();
Modular composition is supported, but it is not required. See the Native AOT sample for a complete application in which the host composes internal handlers from two referenced assemblies.
Samples
All samples target .NET 10. Start with the samples overview, or go directly to one of these applications:
Dependency-injection Minimal API demonstrates reflection-based handler scanning, queries, commands, notifications, FluentValidation pipeline behavior, and internal handlers in Orders and Stock assemblies. Run it with:
dotnet run --project samples/DependencyInjection/Dispatcher.SampleApiNative AOT Minimal API demonstrates generated handler registration, a host-generated dispatcher, an open generic logging behavior, source-generated JSON metadata, and internal handlers composed from two referenced assemblies. Publish it with:
dotnet publish samples/NativeAot/Dispatcher.NativeAotHostSample -c Release
Packages
Send0xx.Dispatcher.Abstractionscontains messages, handlers, pipeline contracts, dispatcher interfaces, andUnit.Send0xx.Dispatchercontains the container-neutral runtime, handler registry, wrappers, and exceptions.Send0xx.Dispatcher.DependencyInjection.Extensionscontains typed, reflection-free Microsoft DI registrations.Send0xx.Dispatcher.DependencyInjectioncontains reflection-based Microsoft DI registration and handler scanning.Send0xx.Dispatcher.SourceGenerationcontains generated registration and dispatch for trimming and Native AOT.
Most applications should reference either Send0xx.Dispatcher.DependencyInjection or Send0xx.Dispatcher.SourceGeneration, not every package individually. Package IDs use the Send0xx prefix, while the public types remain in the Dispatcher namespace.
Performance
The direct handler path avoids pipeline construction when no behavior applies. The runtime resolves behaviors for every dispatch so scoped and transient lifetimes remain correct, and notification handlers execute without a reflection-based dispatch path.
Run the .NET 10 BenchmarkDotNet suite in Release mode:
dotnet run --project benchmarks/Dispatcher.Benchmarks -c Release
The benchmark notes describe the available latency, allocation, pipeline, and implementation comparisons.
Current limitations
- Reflection-based registration is not trimming or Native AOT safe; use
Send0xx.Dispatcher.SourceGenerationfor those deployment modes. - Queries and commands use exact concrete message types and require exactly one handler.
- Notifications execute sequentially rather than concurrently.
- Pipeline behaviors apply to queries and commands, not notifications.
Project
Contributions and design discussions are welcome. See the contribution guide to get started. Maintainers can find CI and NuGet publishing instructions in the release guide.
Dispatcher is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Send0xx.Dispatcher (>= 1.0.0-preview.5)
- Send0xx.Dispatcher.Abstractions (>= 1.0.0-preview.5)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Send0xx.Dispatcher (>= 1.0.0-preview.5)
- Send0xx.Dispatcher.Abstractions (>= 1.0.0-preview.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 1.0.0-preview.5 | 111 | 8/8/2026 | |
| 1.0.0-preview.4 | 102 | 8/8/2026 |