AutoDispatch.Generator 1.4.0

dotnet add package AutoDispatch.Generator --version 1.4.0
                    
NuGet\Install-Package AutoDispatch.Generator -Version 1.4.0
                    
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="AutoDispatch.Generator" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AutoDispatch.Generator" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="AutoDispatch.Generator" />
                    
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 AutoDispatch.Generator --version 1.4.0
                    
#r "nuget: AutoDispatch.Generator, 1.4.0"
                    
#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 AutoDispatch.Generator@1.4.0
                    
#: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=AutoDispatch.Generator&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=AutoDispatch.Generator&version=1.4.0
                    
Install as a Cake Tool

AutoDispatch.Generator

NuGet NuGet Downloads CI License: MIT

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; no IPipelineBehavior<,> magic at runtime
  • No marker interfaces — commands stay as plain POCOs
  • AOT-friendly — everything is compile-time generated
  • DI-readyAddAutoDispatch() wires up handlers, behaviors, and IDispatcher

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:

  1. AutoDispatch.HandlerAttribute
  2. AutoDispatch.IDispatcher
  3. AutoDispatch.Dispatcher
  4. AddAutoDispatch() for IServiceCollection

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 Handle or HandleAsync
  • Handle must have exactly one command parameter
  • HandleAsync may have one command parameter, or a second CancellationToken
  • Methods with zero parameters or more than two parameters are ignored
  • Dispatcher is generated as internal sealed
  • AddAutoDispatch() registers handlers with AddScoped

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 no Handle or HandleAsync methods. 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

HandleAsync on '{Handler}' for command '{Command}' is missing a CancellationToken parameter. Consider adding CancellationToken ct = default as 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 implement AutoDispatch.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 declare public 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.

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

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.

License

MIT

There are no supported framework assets in this package.

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.

Version Downloads Last Updated
1.4.0 28 7/10/2026
1.3.1 73 7/7/2026
1.2.0 104 6/25/2026

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.