EasyDispatch 1.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package EasyDispatch --version 1.0.2
                    
NuGet\Install-Package EasyDispatch -Version 1.0.2
                    
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="EasyDispatch" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EasyDispatch" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="EasyDispatch" />
                    
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 EasyDispatch --version 1.0.2
                    
#r "nuget: EasyDispatch, 1.0.2"
                    
#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 EasyDispatch@1.0.2
                    
#: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=EasyDispatch&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=EasyDispatch&version=1.0.2
                    
Install as a Cake Tool

EasyDispatch

A Mediator implementation for .NET that implements the CQRS and Mediator patterns that aims for ease-of-use, and a relatively simple migration path from Mediatr.

.NET 9.0 License: MIT

Features

  • CQRS Support: Separate interfaces for Commands, Queries, and Notifications
  • Pipeline Behaviors: Extensible middleware pipeline for cross-cutting concerns
  • Flexible Notification Strategies: Four built-in strategies for handling multiple notification handlers
  • Dependency Injection: First-class support for Microsoft.Extensions.DependencyInjection
  • Minimal Dependencies: Only depends on Microsoft.Extensions.DependencyInjection.Abstractions

Installation

# Via NuGet (when published)
dotnet add package EasyDispatch

# Or add to your .csproj
<PackageReference Include="EasyDispatch" Version="1.0.x" />

Quick Start

1. Define Your Messages

using EasyDispatch.Contracts;

// Query - retrieves data
public record GetUserQuery(int UserId) : IQuery<UserDto>;

// Command - performs action, no return value
public record DeleteUserCommand(int UserId) : ICommand;

// Command - performs action, returns value
public record CreateUserCommand(string Name, string Email) : ICommand<int>;

// Notification - pub/sub event
public record UserCreatedNotification(int UserId, string Name) : INotification;

2. Implement Handlers

using EasyDispatch.Handlers;

public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
    private readonly IUserRepository _repository;

    public GetUserQueryHandler(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<UserDto> Handle(GetUserQuery query, CancellationToken cancellationToken)
    {
        var user = await _repository.GetByIdAsync(query.UserId, cancellationToken);
        return new UserDto(user.Id, user.Name, user.Email);
    }
}

public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    private readonly IUserRepository _repository;

    public async Task<int> Handle(CreateUserCommand command, CancellationToken cancellationToken)
    {
        var user = new User { Name = command.Name, Email = command.Email };
        await _repository.AddAsync(user, cancellationToken);
        return user.Id;
    }
}

// Notification handlers - multiple handlers can exist
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
    public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
    {
        // Send welcome email
    }
}

public class AuditLogHandler : INotificationHandler<UserCreatedNotification>
{
    public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
    {
        // Log to audit system
    }
}

3. Register with DI

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// Simple registration - scans assembly for handlers
services.AddMediator(typeof(Program).Assembly);

// With configuration
services.AddMediator(options =>
{
    options.Assemblies = new[] { typeof(Program).Assembly };
    options.HandlerLifetime = ServiceLifetime.Scoped;
    options.NotificationPublishStrategy = NotificationPublishStrategy.ContinueOnException;
})
.AddOpenBehavior(typeof(LoggingBehavior<,>))
.AddOpenBehavior(typeof(ValidationBehavior<,>));

4. Use the Mediator

public class UserController : ControllerBase
{
    private readonly IMediator _mediator;

    public UserController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var user = await _mediator.SendAsync(new GetUserQuery(id));
        return Ok(user);
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
    {
        var userId = await _mediator.SendAsync(
            new CreateUserCommand(request.Name, request.Email));
        
        await _mediator.PublishAsync(new UserCreatedNotification(userId, request.Name));
        
        return CreatedAtAction(nameof(GetUser), new { id = userId }, userId);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteUser(int id)
    {
        await _mediator.SendAsync(new DeleteUserCommand(id));
        return NoContent();
    }
}

Configuration Options

MediatorOptions

services.AddMediator(options =>
{
    // Required: Assemblies to scan for handlers
    options.Assemblies = new[] 
    { 
        typeof(Program).Assembly,
        typeof(HandlersAssembly).Assembly 
    };

    // Optional: Handler lifetime (default: Scoped)
    options.HandlerLifetime = ServiceLifetime.Scoped;

    // Optional: Notification publishing strategy (default: StopOnFirstException)
    options.NotificationPublishStrategy = NotificationPublishStrategy.ParallelWhenAll;

    // Optional: Validate handlers at startup (default: false)
    options.ValidateHandlersAtStartup = false;
});

Notification Publishing Strategies

EasyDispatch provides four strategies for handling multiple notification handlers:

StopOnFirstException (Default)

options.NotificationPublishStrategy = NotificationPublishStrategy.StopOnFirstException;
  • Executes handlers sequentially
  • Stops on first exception
  • Maintains ordering guarantees
  • Use for: Critical operations where any failure should halt execution

ContinueOnException

options.NotificationPublishStrategy = NotificationPublishStrategy.ContinueOnException;
  • Executes handlers sequentially
  • Continues on exceptions
  • Collects all exceptions as AggregateException
  • Use for: Logging, auditing where all handlers should attempt execution

ParallelWhenAll

options.NotificationPublishStrategy = NotificationPublishStrategy.ParallelWhenAll;
  • Executes handlers in parallel
  • Waits for all handlers to complete
  • Collects exceptions as AggregateException
  • Use for: Independent handlers that can run concurrently

ParallelNoWait

options.NotificationPublishStrategy = NotificationPublishStrategy.ParallelNoWait;
  • Fire-and-forget parallel execution
  • Returns immediately without waiting
  • Logs errors but doesn't throw to caller
  • Use for: Non-critical notifications, fire-and-forget events

Pipeline Behaviors

Pipeline behaviors are middleware that wrap handler execution, perfect for cross-cutting concerns.

Creating a Behavior

using EasyDispatch.Behaviors;

public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
{
    private readonly ILogger<LoggingBehavior<TMessage, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TMessage, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TMessage message,
        Func<Task<TResponse>> next,
        CancellationToken cancellationToken)
    {
        var messageName = typeof(TMessage).Name;
        
        _logger.LogInformation("Executing {MessageName}", messageName);
        
        try
        {
            var response = await next();
            _logger.LogInformation("Executed {MessageName} successfully", messageName);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error executing {MessageName}", messageName);
            throw;
        }
    }
}

Registering Behaviors

// Open generic - applies to all messages
services.AddMediator(typeof(Program).Assembly)
    .AddOpenBehavior(typeof(LoggingBehavior<,>))
    .AddOpenBehavior(typeof(ValidationBehavior<,>))
    .AddOpenBehavior(typeof(PerformanceBehavior<,>));

// Specific message type
services.AddMediator(typeof(Program).Assembly)
    .AddBehavior<CreateUserCommand, int, TransactionBehavior>();

Common Behavior Examples

Validation Behavior (FluentValidation)
public class ValidationBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
{
    private readonly IEnumerable<IValidator<TMessage>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TMessage>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TMessage message,
        Func<Task<TResponse>> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TMessage>(message);
        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}
Performance Monitoring
public class PerformanceBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
{
    private readonly ILogger<PerformanceBehavior<TMessage, TResponse>> _logger;
    private readonly Stopwatch _timer;

    public PerformanceBehavior(ILogger<PerformanceBehavior<TMessage, TResponse>> logger)
    {
        _logger = logger;
        _timer = new Stopwatch();
    }

    public async Task<TResponse> Handle(
        TMessage message,
        Func<Task<TResponse>> next,
        CancellationToken cancellationToken)
    {
        _timer.Start();
        var response = await next();
        _timer.Stop();

        var elapsedMilliseconds = _timer.ElapsedMilliseconds;
        if (elapsedMilliseconds > 500)
        {
            _logger.LogWarning(
                "Long Running Request: {MessageName} ({ElapsedMilliseconds} ms)",
                typeof(TMessage).Name,
                elapsedMilliseconds);
        }

        return response;
    }
}
Authorization
public class AuthorizationBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
{
    private readonly ICurrentUserService _currentUserService;
    private readonly IAuthorizationService _authorizationService;

    public async Task<TResponse> Handle(
        TMessage message,
        Func<Task<TResponse>> next,
        CancellationToken cancellationToken)
    {
        var authorizeAttributes = typeof(TMessage)
            .GetCustomAttributes(typeof(AuthorizeAttribute), true)
            .Cast<AuthorizeAttribute>()
            .ToList();

        if (!authorizeAttributes.Any())
            return await next();

        var currentUser = _currentUserService.GetCurrentUser();
        if (currentUser == null)
            throw new UnauthorizedException();

        foreach (var attribute in authorizeAttributes)
        {
            var authorized = await _authorizationService.AuthorizeAsync(
                currentUser, 
                attribute.Policy);
            
            if (!authorized)
                throw new ForbiddenException($"Access denied: {attribute.Policy}");
        }

        return await next();
    }
}

Unit Type for Void Operations

EasyDispatch uses the Unit type internally to represent void operations in pipeline behaviors:

// Void commands use Unit in behaviors
public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
{
    public async Task<TResponse> Handle(
        TMessage message,
        Func<Task<TResponse>> next,
        CancellationToken cancellationToken)
    {
        // Works for both void and non-void commands
        return await next();
    }
}

This allows a single behavior implementation to work across all message types.

Performance

EasyDispatch is built for production with performance in mind:

Metric Value
Cold Start ~100-500µs (first request)
Warm Requests ~1-5µs (cached reflection)
Throughput >10,000 requests/min per core
Memory Overhead <100 bytes per request
Cache Size ~1KB per unique handler type

Performance Optimizations

  • Reflection Caching: All MethodInfo and Type lookups cached
  • Thread-Safe: Uses ConcurrentDictionary for all caches
  • Zero Allocations: Minimal GC pressure per request
  • Efficient Dispatch: Direct method invocation after initial lookup

Architecture

Message Types

IQuery<TResponse>          - Retrieves data, no side effects
ICommand                   - Performs action, void return
ICommand<TResponse>        - Performs action, returns value
INotification              - Pub/sub event, multiple handlers

Handler Types

IQueryHandler<TQuery, TResponse>              - Handles queries
ICommandHandler<TCommand>                     - Handles void commands
ICommandHandler<TCommand, TResponse>          - Handles commands with response
INotificationHandler<TNotification>           - Handles notifications

Pipeline

Request → [Behavior 1] → [Behavior 2] → [Behavior N] → Handler → Response

Error Handling

EasyDispatch provides clear, actionable error messages:

Missing Handler

No handler registered for query 'GetUserQuery'.
Expected a handler implementing IQueryHandler<GetUserQuery, UserDto>.
Did you forget to call AddMediator() with the assembly containing your handlers?

Notification Failures

Depending on your strategy:

  • StopOnFirstException: Throws first exception immediately
  • ContinueOnException: Collects all exceptions in AggregateException
  • ParallelWhenAll: Collects all parallel exceptions in AggregateException
  • ParallelNoWait: Logs errors, doesn't throw

Migration from MediatR

EasyDispatch uses a very similar API to MediatR, making migration straightforward:

// MediatR
public record GetUserQuery(int Id) : IRequest<UserDto>;
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserDto> { }

// EasyDispatch
public record GetUserQuery(int Id) : IQuery<UserDto>;
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto> { }

Main differences:

  • IRequest<T>IQuery<T> or ICommand<T>
  • IRequestHandler<,>IQueryHandler<,> or ICommandHandler<,>
  • Additional notification strategies

Development Setup

# Clone the repository
git clone https://github.com/yourusername/easydispatch.git

# Restore dependencies
dotnet restore

# Build
dotnet build

# Run tests
dotnet test

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"

License

This project is licensed under the MIT License - see the LICENSE file for details.

Resources

Support

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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.7 292 10/9/2025
1.0.6 244 10/8/2025
1.0.5 168 10/7/2025
1.0.2 167 10/7/2025
1.0.1 165 10/7/2025
1.0.0 242 10/7/2025 1.0.0 is deprecated because it has critical bugs.