AutoDispatch.Generator
1.5.0
dotnet add package AutoDispatch.Generator --version 1.5.0
NuGet\Install-Package AutoDispatch.Generator -Version 1.5.0
<PackageReference Include="AutoDispatch.Generator" Version="1.5.0" />
<PackageVersion Include="AutoDispatch.Generator" Version="1.5.0" />
<PackageReference Include="AutoDispatch.Generator" />
paket add AutoDispatch.Generator --version 1.5.0
#r "nuget: AutoDispatch.Generator, 1.5.0"
#:package AutoDispatch.Generator@1.5.0
#addin nuget:?package=AutoDispatch.Generator&version=1.5.0
#tool nuget:?package=AutoDispatch.Generator&version=1.5.0
AutoDispatch.Generator
AutoDispatch gives you the MediatR-style handler pattern without IRequest<T>, IRequestHandler<,>, reflection, or runtime dispatch overhead. Mark a handler with [Handler], write Handle or HandleAsync, and the generator emits a strongly-typed dispatcher at build time.
Why AutoDispatch?
- Same mental model as MediatR — command/query + handler + dispatcher
- Zero reflection — direct generated calls, no runtime dispatch overhead
- Pipeline behaviors —
[Behavior(Order = N)]wraps all async handlers at compile time; noIPipelineBehavior<,>magic at runtime - No marker interfaces — commands stay as plain POCOs
- AOT-friendly — everything is compile-time generated
- DI-ready —
AddAutoDispatch()wires up handlers, behaviors, andIDispatcher
Installation
dotnet add package AutoDispatch.Generator
Then register the generated dispatcher:
builder.Services.AddAutoDispatch();
Before vs After
MediatR-style boilerplate
using MediatR;
public sealed record CreateOrderCommand(string CustomerId) : IRequest<OrderId>;
public sealed class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderId>
{
public Task<OrderId> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// ...
}
}
AutoDispatch
using AutoDispatch;
public sealed record CreateOrderCommand(string CustomerId);
[Handler]
public sealed class CreateOrderHandler
{
public Task<OrderId> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
{
// ...
}
}
What gets generated
Given one or more [Handler] classes, AutoDispatch emits:
AutoDispatch.HandlerAttributeAutoDispatch.IDispatcherAutoDispatch.DispatcherAddAutoDispatch()forIServiceCollection
Example generated dispatcher:
#nullable enable
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace AutoDispatch
{
public interface IDispatcher
{
Task<OrderId> SendAsync(CreateOrderCommand command, CancellationToken ct = default);
void Send(DeleteOrderCommand command);
}
internal sealed class Dispatcher : IDispatcher
{
private readonly IServiceProvider _sp;
public Dispatcher(IServiceProvider sp) => _sp = sp;
public Task<OrderId> SendAsync(CreateOrderCommand command, CancellationToken ct = default)
=> _sp.GetRequiredService<CreateOrderHandler>().HandleAsync(command, ct);
public void Send(DeleteOrderCommand command)
=> _sp.GetRequiredService<DeleteOrderHandler>().Handle(command);
}
}
Conventions
AutoDispatch discovers public instance non-static methods on classes marked with [Handler].
Supported signatures:
| Handler method | Generated dispatcher method |
|---|---|
T Handle(TCommand cmd) |
T Send(TCommand command) |
void Handle(TCommand cmd) |
void Send(TCommand command) |
Task HandleAsync(TCommand cmd, CancellationToken ct = default) |
Task SendAsync(TCommand command, CancellationToken ct = default) |
Task<T> HandleAsync(TCommand cmd, CancellationToken ct = default) |
Task<T> SendAsync(TCommand command, CancellationToken ct = default) |
Task HandleAsync(TCommand cmd) |
Task SendAsync(TCommand command, CancellationToken ct = default) |
Task<T> HandleAsync(TCommand cmd) |
Task<T> SendAsync(TCommand command, CancellationToken ct = default) |
Rules:
- Only methods named exactly
HandleorHandleAsync Handlemust have exactly one command parameterHandleAsyncmay have one command parameter, or a secondCancellationToken- Methods with zero parameters or more than two parameters are ignored
Dispatcheris generated asinternal sealedAddAutoDispatch()registers handlers withAddScoped
Semantic aliases
[CommandHandler] and [QueryHandler] are aliases for [Handler] — use whichever reads best in your codebase.
[CommandHandler]
public sealed class CreateOrderHandler
{
public Task<OrderId> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
=> Task.FromResult(new OrderId(Guid.NewGuid()));
}
[QueryHandler]
public sealed class GetOrderHandler
{
public Task<Order?> HandleAsync(GetOrderQuery query, CancellationToken ct = default)
=> Task.FromResult<Order?>(null);
}
All three attributes are equivalent — the generated code is identical.
Usage
using AutoDispatch;
public sealed record CreateOrderCommand(string CustomerId);
public sealed record DeleteOrderCommand(Guid OrderId);
public sealed record OrderId(Guid Value);
[Handler]
public sealed class CreateOrderHandler
{
public Task<OrderId> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
=> Task.FromResult(new OrderId(Guid.NewGuid()));
}
[Handler]
public sealed class DeleteOrderHandler
{
public void Handle(DeleteOrderCommand command)
{
}
}
Then consume the generated dispatcher:
app.MapPost("/orders", async (CreateOrderCommand command, AutoDispatch.IDispatcher dispatcher, CancellationToken ct) =>
{
var orderId = await dispatcher.SendAsync(command, ct);
return Results.Ok(orderId);
});
Generated DI registration
builder.Services.AddAutoDispatch();
Produces code like:
services.AddScoped<CreateOrderHandler>();
services.AddScoped<DeleteOrderHandler>();
services.AddScoped<AutoDispatch.IDispatcher, AutoDispatch.Dispatcher>();
Pipeline behaviors
[Behavior(Order = N)] wraps all async handlers in a compile-time pipeline. Identical mental model to MediatR's IPipelineBehavior<,> — but the chain is emitted as generated code, not resolved via reflection at runtime.
Behavior requirements:
- The behavior class must be public, non-abstract, and open-generic with exactly two type parameters
- It must implement
IPipelineBehavior<TCommand, TResult> - It must expose
public Task<TResult> HandleAsync(TCommand command, Func<Task<TResult>> next, CancellationToken ct = default)
Define a behavior
using AutoDispatch;
[Behavior(Order = 0)]
public sealed class LoggingBehavior<TCommand, TResult>
: IPipelineBehavior<TCommand, TResult>
{
private readonly ILogger<LoggingBehavior<TCommand, TResult>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TCommand, TResult>> logger)
=> _logger = logger;
public async Task<TResult> HandleAsync(
TCommand command,
Func<Task<TResult>> next,
CancellationToken ct = default)
{
_logger.LogInformation("→ {Command}", typeof(TCommand).Name);
var result = await next();
_logger.LogInformation("← {Command}", typeof(TCommand).Name);
return result;
}
}
That's all. AddAutoDispatch() registers it automatically.
Multiple behaviors
[Behavior(Order = 0)] // runs first (outermost)
public sealed class LoggingBehavior<TCmd, TResult> : IPipelineBehavior<TCmd, TResult> { ... }
[Behavior(Order = 1)] // runs second
public sealed class ValidationBehavior<TCmd, TResult> : IPipelineBehavior<TCmd, TResult> { ... }
[Behavior(Order = 2)] // runs last (innermost, just before the handler)
public sealed class TimingBehavior<TCmd, TResult> : IPipelineBehavior<TCmd, TResult> { ... }
Execution order: Logging → Validation → Timing → Handler → Timing → Validation → Logging.
When multiple behaviors have the same Order, AutoDispatch preserves declaration order.
What gets generated
For Task<OrderId> SendAsync(CreateOrderCommand) with two behaviors:
// Generated dispatcher method:
public Task<OrderId> SendAsync(CreateOrderCommand command, CancellationToken ct = default)
{
Func<Task<OrderId>> pipeline =
() => _sp.GetRequiredService<CreateOrderHandler>().HandleAsync(command, ct);
var _b1 = _sp.GetRequiredService<TimingBehavior<CreateOrderCommand, OrderId>>();
var _p1 = pipeline;
pipeline = () => _b1.HandleAsync(command, _p1, ct);
var _b0 = _sp.GetRequiredService<LoggingBehavior<CreateOrderCommand, OrderId>>();
var _p0 = pipeline;
pipeline = () => _b0.HandleAsync(command, _p0, ct);
return pipeline();
}
Behaviors and void-async handlers
For Task (no result) handlers, the generator wraps the call in Task<Unit> internally. Unit is emitted by the generator — you never reference it directly; the method signature stays Task SendAsync(...).
Behaviors can also short-circuit by returning a result without calling next().
Behaviors only apply to async handlers
Sync T Send(...) and void Send(...) methods are not wrapped. Add a pipeline when you migrate a sync handler to async, or keep it sync for zero overhead.
Diagnostics
| Code | Severity | Description |
|---|---|---|
| AD001 | Warning | [Handler] on a class with no valid Handle/HandleAsync methods |
| AD002 | Error | Duplicate handlers discovered for the same command type |
| AD003 | Warning | HandleAsync does not accept CancellationToken |
| AD004 | Error | [Behavior] type is not a public, non-abstract open generic class with exactly two type parameters |
| AD005 | Error | [Behavior] type does not implement IPipelineBehavior<TCommand, TResult> |
| AD006 | Error | [Behavior] type does not expose a valid public HandleAsync method |
AD001
[Handler]on '{Type}' has noHandleorHandleAsyncmethods. No dispatch methods will be generated.
Add a valid Handle or HandleAsync method to the handler class.
AD002
Duplicate handler for command '{Command}': both '{HandlerA}' and '{HandlerB}' define a Handle/HandleAsync method for this command type. Remove one handler or rename the method.
Each command/query type must map to exactly one handler method.
AD003
HandleAsyncon '{Handler}' for command '{Command}' is missing aCancellationTokenparameter. Consider addingCancellationToken ct = defaultas the second parameter.`
The method still works; the warning helps you preserve cancellation flow.
AD004
[Behavior]on '{Type}' must be a public, non-abstract class with exactly two type parameters so AutoDispatch can close it as<TCommand, TResult>.`
Pipeline behaviors are resolved as closed generics at dispatch time, so [Behavior] types must be declared as open generic classes such as LoggingBehavior<TCommand, TResult>.
AD005
[Behavior]on '{Type}' must implementAutoDispatch.IPipelineBehavior<TCommand, TResult>using its declared type parameters.`
Implement the generated IPipelineBehavior<TCommand, TResult> interface directly on the behavior type.
AD006
[Behavior]on '{Type}' must declarepublic Task<TResult> HandleAsync(TCommand command, Func<Task<TResult>> next, CancellationToken ct = default).`
Explicit interface implementations are not enough — the generated dispatcher calls the behavior's public HandleAsync method directly.
XML doc comments and pipeline readability
Doc comments on Handle/HandleAsync methods are forwarded to the generated IDispatcher member automatically:
[Handler]
public sealed class CreateOrderHandler
{
/// <summary>Creates an order for the given customer.</summary>
public Task<OrderId> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
=> Task.FromResult(new OrderId(Guid.NewGuid()));
}
generates:
public interface IDispatcher
{
/// <summary>Creates an order for the given customer.</summary>
Task<OrderId> SendAsync(CreateOrderCommand command, CancellationToken ct = default);
}
Generated async dispatch methods that go through a behavior pipeline are also annotated with a comment showing the execution order, so you never have to guess:
// Pipeline: LoggingBehavior -> ValidationBehavior -> CreateOrderHandler.HandleAsync -> LoggingBehavior -> ValidationBehavior
public Task<OrderId> SendAsync(CreateOrderCommand command, CancellationToken ct = default)
{
...
}
IDE code fixes
AutoDispatch.CodeFixes ships inside the AutoDispatch.Generator package and adds one-click fixes:
| Diagnostic | Quick fix |
|---|---|
| AD001 | Adds a HandleAsync stub method to a [Handler] class with none |
| AD003 | Adds the missing CancellationToken ct = default parameter |
Testing handlers and behaviors
The AutoDispatch.Testing package makes it
easy to unit test handlers and [Behavior] chains without a DI container:
dotnet add package AutoDispatch.Testing
// FakeServiceProvider — a minimal IServiceProvider for constructing the generated Dispatcher
var sp = new FakeServiceProvider().Add(new CreateOrderHandler());
IDispatcher dispatcher = new Dispatcher(sp);
var orderId = await dispatcher.SendAsync(new CreateOrderCommand("cust-1"));
// PipelineTestHarness — test a behavior in isolation, short-circuiting next()
var result = await PipelineTestHarness.InvokeAsync<CreateOrderCommand, OrderId>(
loggingBehavior.HandleAsync,
command,
nextResult: expectedOrderId);
See the AutoDispatch.Testing README for more.
Scaffolding with dotnet new
dotnet new install AutoDispatch.Templates
dotnet new autodispatch-handler -n CreateOrder --namespace MyApp.Orders
Generates a ready-to-fill CreateOrderCommand.cs with the command record and [Handler] class.
AutoDispatch vs alternatives
| Approach | Boilerplate | Runtime dispatch | Pipeline behaviors | Compile-time safety | AOT |
|---|---|---|---|---|---|
| AutoDispatch | Low | None | Compile-time generated | High | ✅ |
| MediatR | Medium | Yes | Runtime reflection | High | ⚠️ |
| Raw service calls | Low | None | Manual | High | ✅ |
Benchmarks
BenchmarkDotNet results for a single no-op
handler, comparing the generated IDispatcher against MediatR's IMediator:
| Method | Mean | Ratio | Allocated | Alloc Ratio |
|---|---|---|---|---|
| AutoDispatch_SendAsync | 23.42 ns | 1.00 | 96 B | 1.00 |
| MediatR_Send | 89.13 ns | 3.85 | 288 B | 3.00 |
~3.8x faster, 3x fewer allocations — no reflection-based handler lookup, no runtime-built
pipeline. Run it yourself with dotnet run -c Release in benchmarks/AutoDispatch.Benchmarks.
Migrating from MediatR
AutoDispatch follows the same CQRS mental model as MediatR, so migration is mechanical:
1. Install AutoDispatch and remove MediatR
dotnet add package AutoDispatch.Generator
dotnet remove package MediatR
dotnet remove package MediatR.Extensions.Microsoft.DependencyInjection
2. Remove marker interfaces from commands
// Before
public sealed record CreateOrderCommand(string CustomerId) : IRequest<OrderId>;
// After
public sealed record CreateOrderCommand(string CustomerId);
3. Convert handler classes
// Before
public sealed class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderId>
{
public Task<OrderId> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
=> Task.FromResult(new OrderId(Guid.NewGuid()));
}
// After
[Handler]
public sealed class CreateOrderHandler
{
public Task<OrderId> HandleAsync(CreateOrderCommand command, CancellationToken ct = default)
=> Task.FromResult(new OrderId(Guid.NewGuid()));
}
4. Convert pipeline behaviors
// Before
public sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
_logger.LogInformation("→ {Request}", typeof(TRequest).Name);
var result = await next();
_logger.LogInformation("← {Request}", typeof(TRequest).Name);
return result;
}
}
// After
[Behavior(Order = 0)]
public sealed class LoggingBehavior<TCommand, TResult>
: IPipelineBehavior<TCommand, TResult>
{
public async Task<TResult> HandleAsync(TCommand command, Func<Task<TResult>> next, CancellationToken ct = default)
{
_logger.LogInformation("→ {Command}", typeof(TCommand).Name);
var result = await next();
_logger.LogInformation("← {Command}", typeof(TCommand).Name);
return result;
}
}
5. Update DI registration
// Before
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());
// After
builder.Services.AddAutoDispatch();
6. Update dispatch call sites
// Before (IMediator)
var orderId = await mediator.Send(new CreateOrderCommand(customerId), ct);
// After (IDispatcher)
var orderId = await dispatcher.SendAsync(new CreateOrderCommand(customerId), ct);
Tip: Use the AutoDispatch Migrator Copilot agent to automate the migration across your entire codebase.
Best fit
Use AutoDispatch when you want:
- CQRS-style organization without MediatR ceremony
- Build-time generated dispatch code
- Fast startup and predictable runtime behavior
- Plain C# command/query types with no framework coupling
Also by the same author
🌐 Full suite overview: swevo.github.io
| Package | Description |
|---|---|
| AutoWire | Compile-time DI auto-registration for Microsoft.Extensions.DependencyInjection. |
| AutoMap.Generator | Compile-time object mapping with generated extension methods. |
| AutoValidate.Generator | Compile-time validator discovery and registration. |
| AutoResult.Generator | Compile-time result helpers and Try*() wrappers. |
| AutoQuery.Generator | Compile-time query specifications for LINQ-based filtering. |
| AutoLog.Generator | Compile-time high-performance logging — [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe. |
| AutoHttpClient.Generator | Compile-time typed HTTP client — [HttpClient] on an interface generates a strongly-typed client. AOT-safe Refit alternative. |
Related Packages
| Package | Downloads | Description |
|---|---|---|
| AutoWire | Compile-time dependency injection auto-registration for | |
| AutoMap.Generator | Compile-time object mapping for | |
| AutoQuery.Generator | Compile-time query composition for IQueryable using Roslyn incremental source generators | |
| AutoArchitecture | Compile-time architecture/dependency-rule enforcement for | |
| AutoHttpClient.Generator | Compile-time typed HTTP client generation for | |
| AutoLog.Generator | Compile-time high-performance logging for | |
| AutoValidate.Generator | Compile-time FluentValidation wiring for |
License
MIT
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.5.0: XML doc comments on Handle/HandleAsync methods are now forwarded to the generated IDispatcher members; generated async dispatch methods with behaviors now include a "// Pipeline: A -> B -> Handler -> B -> A" comment showing execution order; added IDE code fixes for AD001 (add HandleAsync stub) and AD003 (add CancellationToken parameter) via the companion AutoDispatch.CodeFixes assembly shipped in the same package. 1.4.0: Validated pipeline behaviors with ordered execution, declaration-order tie-breaking, and AD004/AD005/AD006 diagnostics for misconfigured behaviors. 1.3.1: Fix package icon (was showing an incorrect/placeholder image). 1.3.0: [Behavior(Order=N)] pipeline behaviors; IPipelineBehavior<TCommand,TResult>; Unit struct for void-async handlers; open-generic DI registration. 1.2.0: [CommandHandler]/[QueryHandler] semantic aliases. 1.1.0: HandlerLifetime (Scoped/Singleton/Transient). 1.0.0: [Handler] attribute, Handle/HandleAsync discovery, typed IDispatcher + Dispatcher generated, AddAutoDispatch() DI registration, AD001/AD002/AD003 diagnostics.