Send0xx.Dispatcher.DependencyInjection.Extensions 1.0.0-preview.5

Suggested Alternatives

Send0xx.Dispatcher.DependencyInjection

Additional Details

Package was merged into the main DI package in preview.7

This is a prerelease version of Send0xx.Dispatcher.DependencyInjection.Extensions.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Send0xx.Dispatcher.DependencyInjection.Extensions" Version="1.0.0-preview.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Send0xx.Dispatcher.DependencyInjection.Extensions" Version="1.0.0-preview.5" />
                    
Directory.Packages.props
<PackageReference Include="Send0xx.Dispatcher.DependencyInjection.Extensions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Send0xx.Dispatcher.DependencyInjection.Extensions --version 1.0.0-preview.5
                    
#r "nuget: Send0xx.Dispatcher.DependencyInjection.Extensions, 1.0.0-preview.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Send0xx.Dispatcher.DependencyInjection.Extensions@1.0.0-preview.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Send0xx.Dispatcher.DependencyInjection.Extensions&version=1.0.0-preview.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Send0xx.Dispatcher.DependencyInjection.Extensions&version=1.0.0-preview.5&prerelease
                    
Install as a Cake Tool

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.SampleApi
    
  • Native 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.Abstractions contains messages, handlers, pipeline contracts, dispatcher interfaces, and Unit.
  • Send0xx.Dispatcher contains the container-neutral runtime, handler registry, wrappers, and exceptions.
  • Send0xx.Dispatcher.DependencyInjection.Extensions contains typed, reflection-free Microsoft DI registrations.
  • Send0xx.Dispatcher.DependencyInjection contains reflection-based Microsoft DI registration and handler scanning.
  • Send0xx.Dispatcher.SourceGeneration contains 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.SourceGeneration for 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.