Skip to content
TanNguyenNetPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Mediator

NuGet .NET 10.0 License: MIT Tests

High-performance Mediator pattern implementation for .NET with CQRS support. Inspired by MediatR with focus on performance and simplicity.

✨ Features

  • πŸš€ High Performance - Static caching, minimal allocations, aggressive inlining, zero-allocation publish for synchronous handlers
  • πŸ“¦ CQRS Ready - Request/Response, Commands, Queries, and Notifications
  • πŸ”Œ Pipeline Behaviors - Cross-cutting concerns (logging, validation, caching)
  • ⚑ Streaming Support - IAsyncEnumerable for large datasets
  • πŸ›‘οΈ Validation - Built-in validation behavior with custom validators
  • 🎯 Pre/Post Processors - Execute logic before/after handlers
  • πŸ’‰ DI Integration - Native Microsoft.Extensions.DependencyInjection support

πŸ“¦ Installation

dotnet add package Mediator.Slim

DI registration (AddMediator) is included in the package β€” no extra package needed.

Requirements: .NET 10.0 or later. Use version 1.0.1 for .NET 8.0.

πŸ†• What's New in 2.0.0

  • ⬆️ .NET 10 - Targets net10.0 (breaking: .NET 8 is no longer supported) with Microsoft.Extensions.* 10.0 packages.
  • 🧬 Polymorphic publish - Publish dispatches on the notification's runtime type, so publishing through a base type (e.g. DomainEventBase) or as object reaches the concrete type's handlers.
  • ⚑ Faster notifications - Publishing to 3+ handlers is non-async, uses ArrayPool, and allocates nothing when every handler completes synchronously.
Handlers (sync) 1.0.1 2.0.0
3 58 ns / 144 B 16 ns / 0 B
10 160 ns / 144 B 37 ns / 0 B
50 697 ns / 144 B 182 ns / 0 B

BenchmarkDotNet on .NET 10, publish fan-out to N handlers; the 1.0.1 column is the 1.0.1 code path run on .NET 10. For async handlers time is unchanged and allocations are lower.

πŸš€ Quick Start

1. Define a Request and Handler

// Request
public record GetUserQuery(int UserId) : IRequest<UserDto>;

// Handler
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto>
{
    public async Task<UserDto> Handle(GetUserQuery request, CancellationToken ct)
    {
        return new UserDto(request.UserId, "John Doe");
    }
}

2. Register Services

services.AddMediator(config =>
{
    config.AddAssembly(typeof(Program).Assembly);
});

3. Send Requests

public class UserController
{
    private readonly IMediator _mediator;

    public UserController(IMediator mediator) => _mediator = mediator;

    public async Task<UserDto> GetUser(int id)
    {
        return await _mediator.Send(new GetUserQuery(id));
    }
}

πŸ“– Usage Examples

Commands (No Response)

public record CreateUserCommand(string Name, string Email) : IRequest;

public class CreateUserHandler : IRequestHandler<CreateUserCommand, Unit>
{
    public async Task<Unit> Handle(CreateUserCommand request, CancellationToken ct)
    {
        // Create user logic
        return Unit.Value;
    }
}

Notifications (Pub/Sub)

public record UserCreated(int UserId, string Name) : INotification;

public class SendWelcomeEmail : INotificationHandler<UserCreated>
{
    public async Task Handle(UserCreated notification, CancellationToken ct)
    {
        // Send email
    }
}

public class LogUserCreated : INotificationHandler<UserCreated>
{
    public async Task Handle(UserCreated notification, CancellationToken ct)
    {
        // Log event
    }
}

// Usage
await _mediator.Publish(new UserCreated(1, "John"));

All handlers are started and awaited together; Publish completes when every handler has completed and throws if any handler fails.

Publishing through a base type dispatches to the handlers of the runtime type:

public abstract record DomainEventBase : INotification;
public record OrderCreated(int OrderId) : DomainEventBase;

DomainEventBase domainEvent = new OrderCreated(42);
await _mediator.Publish(domainEvent); // invokes INotificationHandler<OrderCreated>

Streaming (IAsyncEnumerable)

public record GetLogsStream(DateTime From) : IStreamRequest<LogEntry>;

public class GetLogsHandler : IStreamRequestHandler<GetLogsStream, LogEntry>
{
    public async IAsyncEnumerable<LogEntry> Handle(
        GetLogsStream request,
        [EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var log in _db.GetLogsAsync(request.From, ct))
        {
            yield return log;
        }
    }
}

Pipeline Behaviors

public class LoggingBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        _logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
        
        var response = await next();
        
        _logger.LogInformation("Handled {Request}", typeof(TRequest).Name);
        return response;
    }
}

// Registration
services.AddMediator(config =>
{
    config.AddAssembly(typeof(Program).Assembly);
    config.AddBehavior(typeof(LoggingBehavior<,>));
});

Validation

public class CreateUserValidator : IValidator<CreateUserCommand>
{
    public Task<ValidationResult> ValidateAsync(
        CreateUserCommand request, 
        CancellationToken ct)
    {
        if (string.IsNullOrEmpty(request.Name))
            return Task.FromResult(
                ValidationResult.Failure("Name", "Name is required"));
        
        return Task.FromResult(ValidationResult.Success);
    }
}

// Registration
services.AddMediator(config =>
{
    config.AddAssembly(typeof(Program).Assembly);
    config.AddBehavior(typeof(ValidationBehavior<,>));
});
services.AddTransient<IValidator<CreateUserCommand>, CreateUserValidator>();

βš™οΈ Configuration

services.AddMediator(config =>
{
    // Scan assemblies for handlers
    config.AddAssembly(typeof(Program).Assembly);
    
    // Configure lifetimes
    config.MediatorLifetime = ServiceLifetime.Scoped;
    config.HandlerLifetime = ServiceLifetime.Transient;
    config.BehaviorLifetime = ServiceLifetime.Transient;
    
    // Add behaviors (executed in order)
    config.AddBehavior(typeof(LoggingBehavior<,>));
    config.AddBehavior(typeof(ValidationBehavior<,>));
    config.AddBehavior(typeof(RequestPreProcessorBehavior<,>));
    config.AddBehavior(typeof(RequestPostProcessorBehavior<,>));
});

πŸ§ͺ Testing

The library includes 84 comprehensive unit tests (xUnit v3) covering:

  • βœ… Request/Response handling
  • βœ… Notifications (single, multiple and 3+ handlers, polymorphic publish)
  • βœ… Pipeline behaviors
  • βœ… Pre/Post processors
  • βœ… Validation
  • βœ… Streaming with IAsyncEnumerable
  • βœ… Cancellation support
  • βœ… Exception handling
dotnet test

global.json opts dotnet test into Microsoft.Testing.Platform, which xUnit v3 requires on the .NET 10 SDK.

πŸ“Š Project Structure

Mediator/
β”œβ”€β”€ src/
β”‚   └── Mediator/                          # NuGet package: Mediator.Slim
β”‚       β”œβ”€β”€ Abstractions/                  # Interfaces
β”‚       β”œβ”€β”€ Behaviors/                     # Pipeline behaviors
β”‚       β”œβ”€β”€ Exceptions/                    # Custom exceptions
β”‚       β”œβ”€β”€ Validation/                    # Validation support
β”‚       β”œβ”€β”€ Wrappers/                      # Handler wrappers
β”‚       β”œβ”€β”€ Mediator.cs                    # Main implementation
β”‚       β”œβ”€β”€ MediatorServiceConfiguration.cs
β”‚       β”œβ”€β”€ ServiceCollectionExtensions.cs # AddMediator registration
β”‚       └── Unit.cs                        # Unit type for void returns
β”œβ”€β”€ tests/
β”‚   └── Mediator.Tests/                    # Unit tests
└── benchmarks/
    └── Mediator.Benchmarks/               # BenchmarkDotNet vs MediatR

🀝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

πŸ“„ License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages