CustomDispatcher 1.0.0

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

CustomDispatcher

A small CQRS-first command/query dispatcher for .NET with dispatch middleware support.

What is CustomDispatcher?

CustomDispatcher is a lightweight, explicit CQRS-style dispatcher library for .NET applications. It separates commands and queries, supports dispatch middleware for cross-cutting concerns, and integrates seamlessly with Microsoft.Extensions.DependencyInjection.

Installation

dotnet add package CustomDispatcher

Quick Start

1. Register CustomDispatcher

builder.Services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

2. Define Commands and Queries

// Command with result
public record CreateUserCommand(string Name, string Email) : ICommand<UserId>;
public record UserId(Guid Value);

// Command without result
public record DeleteUserCommand(Guid Id) : ICommand;

// Query
public record GetUserByIdQuery(Guid Id) : IQuery<UserDto>;
public record UserDto(Guid Id, string Name, string Email);

3. Implement Handlers

public class CreateUserCommandHandler : ICommandProcessor<CreateUserCommand, UserId>
{
    public Task<UserId> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
    {
        // Create user and return ID
        return Task.FromResult(new UserId(Guid.NewGuid()));
    }
}

public class DeleteUserCommandHandler : ICommandProcessor<DeleteUserCommand>
{
    public Task HandleAsync(DeleteUserCommand command, CancellationToken cancellationToken = default)
    {
        // Delete user
        return Task.CompletedTask;
    }
}

public class GetUserByIdQueryHandler : IQueryProcessor<GetUserByIdQuery, UserDto>
{
    public Task<UserDto> HandleAsync(GetUserByIdQuery query, CancellationToken cancellationToken = default)
    {
        // Fetch and return user
        return Task.FromResult(new UserDto(query.Id, "John", "john@example.com"));
    }
}

4. Dispatch from Your Endpoints

app.MapPost("/users", async (CreateUserCommand command, ICommandDispatcher dispatcher) =>
{
    var userId = await dispatcher.DispatchAsync<CreateUserCommand, UserId>(command);
    return Results.Created($"/users/{userId.Value}", userId);
});

app.MapDelete("/users/{id}", async (Guid id, ICommandDispatcher dispatcher) =>
{
    await dispatcher.DispatchAsync(new DeleteUserCommand(id));
    return Results.NoContent();
});

app.MapGet("/users/{id}", async (Guid id, IQueryDispatcher dispatcher) =>
{
    var user = await dispatcher.DispatchAsync<GetUserByIdQuery, UserDto>(new GetUserByIdQuery(id));
    return Results.Ok(user);
});

Dispatch Middleware

Pipeline middleware allow cross-cutting concerns around handlers.

1. Create Middleware

public class ValidationMiddleware<TRequest, TResult> : IDispatchMiddleware<TRequest, TResult>
{
    public async Task<TResult> HandleAsync(
        TRequest request,
        DispatchContinuation<TResult> next,
        CancellationToken cancellationToken = default)
    {
        // Pre-processing (e.g., validation)
        Console.WriteLine($"Before handling: {request}");

        var result = await next();

        // Post-processing
        Console.WriteLine($"After handling: {result}");

        return result;
    }
}

2. Register Middleware

builder.Services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(typeof(Program).Assembly);
    options.AddDispatchMiddleware(typeof(ValidationMiddleware<,>));
    options.AddDispatchMiddleware(typeof(LoggingMiddleware<,>));
});

Middleware execute in registration order:

ValidationMiddleware -> LoggingMiddleware -> Handler

First registered is outermost, last registered is closest to the handler.

FluentValidation Integration

An optional package provides seamless FluentValidation integration.

1. Install the Package

dotnet add package CustomDispatcher.FluentValidation

2. Create Validators

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required.");
        RuleFor(x => x.Email).NotEmpty().WithMessage("Email is required.");
        RuleFor(x => x.Email).EmailAddress().WithMessage("Email is not valid.");
    }
}

3. Register Validation

builder.Services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(typeof(Program).Assembly);
    options.AddDispatchMiddleware(typeof(ValidationMiddleware<,>));
});

builder.Services.AddCustomDispatcherValidation(options =>
{
    options.RegisterValidatorsFromAssembly(typeof(Program).Assembly);
});

Validation runs automatically before the handler. If validation fails, a ValidationException is thrown with all failures.

Dependency Injection Registration

Basic Registration

services.AddCustomDispatcher();

With Assembly Scanning

services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

Multiple Assemblies

services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(
        typeof(ApplicationAssembly).Assembly,
        typeof(DomainAssembly).Assembly);
});

With Dispatch Middleware

services.AddCustomDispatcher(options =>
{
    options.RegisterServicesFromAssembly(typeof(Program).Assembly);
    options.AddDispatchMiddleware(typeof(ValidationMiddleware<,>));
    options.AddDispatchMiddleware(typeof(LoggingMiddleware<,>));
    options.AddDispatchMiddleware(typeof(TransactionMiddleware<,>));
});

Error Handling

The library throws clear, actionable exceptions for infrastructure and configuration errors:

Exception When
DispatchTargetNotFoundException No handler registered for a command or query
MultipleDispatchTargetsFoundException Multiple handlers registered for the same command or query
InvalidMiddlewareRegistrationException Invalid dispatch middleware registration
CustomDispatcherException Base exception type for all CustomDispatcher errors

All exceptions bubble up from handlers unless a user-defined middleware handles them.

Public API

Commands

public interface ICommand { }
public interface ICommand<TResult> { }

Queries

public interface IQuery<TResult> { }

Handlers

public interface ICommandProcessor<in TCommand>
    where TCommand : ICommand
{
    Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

public interface ICommandProcessor<in TCommand, TResult>
    where TCommand : ICommand<TResult>
{
    Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

public interface IQueryProcessor<in TQuery, TResult>
    where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}

Dispatchers

public interface ICommandDispatcher
{
    Task DispatchAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
        where TCommand : ICommand;

    Task<TResult> DispatchAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken = default)
        where TCommand : ICommand<TResult>;
}

public interface IQueryDispatcher
{
    Task<TResult> DispatchAsync<TQuery, TResult>(TQuery query, CancellationToken cancellationToken = default)
        where TQuery : IQuery<TResult>;
}

Pipeline

public delegate Task<TResult> DispatchContinuation<TResult>();

public interface IDispatchMiddleware<in TRequest, TResult>
{
    Task<TResult> HandleAsync(
        TRequest request,
        DispatchContinuation<TResult> next,
        CancellationToken cancellationToken = default);
}

Non-Goals (V1)

These are intentionally not included in V1:

  • Notifications / domain events
  • Event sourcing
  • Outbox pattern
  • Built-in Result/Error types
  • Built-in validation
  • Built-in logging
  • Built-in transaction management
  • Source generators
  • AOT-specific support
  • OpenTelemetry integration
  • Retry policies
  • Background job dispatching
  • Message bus integration

Versioning Policy

CustomDispatcher follows semantic versioning. Before 1.0, the API may change but will be documented. After 1.0, breaking changes require a major version bump.

License

MIT License. See LICENSE for details.

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 was computed.  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.

Version Downloads Last Updated
1.0.0 38 5/26/2026