Indiko.Blocks.Mediation.SimpleMediator 2.1.1

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

Indiko.Blocks.Mediation.SimpleMediator

Lightweight, zero-dependency mediator implementation for applications that need CQRS patterns without external library overhead.

Overview

This package provides a simple, performant implementation of the mediator pattern built specifically for the Indiko framework, offering CQRS support without the complexity or dependencies of larger libraries.

Features

  • Zero External Dependencies: No third-party mediator libraries required
  • Lightweight: Minimal overhead and fast execution
  • CQRS Support: Commands and queries with dedicated interfaces
  • Pipeline Behaviors: Extensible pipeline for cross-cutting concerns
  • Notifications: Pub/sub pattern for domain events
  • Dependency Injection: First-class DI support
  • Simple & Fast: Optimized for performance with minimal abstractions
  • Easy Debugging: Straightforward code flow, easy to trace

Installation

dotnet add package Indiko.Blocks.Mediation.SimpleMediator

Quick Start

Configure Services

using Indiko.Blocks.Mediation.SimpleMediator;

public class Startup : WebStartup
{
    public override void ConfigureServices(IServiceCollection services)
    {
        base.ConfigureServices(services);
        
        // Register SimpleMediator
        services.AddSimpleMediator(options =>
        {
            // Scan assembly for handlers
            options.RegisterHandlersFromAssembly(typeof(Startup).Assembly);
        });
        
        // Register handlers manually (optional)
        services.AddScoped<IRequestHandler<CreateUserCommand, Guid>, CreateUserCommandHandler>();
        services.AddScoped<IRequestHandler<GetUserByIdQuery, UserDto>, GetUserByIdQueryHandler>();
        
        // Register pipeline behaviors
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    }
}

Usage Examples

Commands

// Define command
public class CreateUserCommand : ICommand<Guid>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

// Implement handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateUserCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork)
    {
        _userRepository = userRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        var user = new User
        {
            Id = Guid.NewGuid(),
            FirstName = request.FirstName,
            LastName = request.LastName,
            Email = request.Email,
            CreatedAt = DateTime.UtcNow
        };
        
        await _userRepository.AddAsync(user, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        
        return user.Id;
    }
}

// Use in controller
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

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

    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserCommand command)
    {
        var userId = await _mediator.Send<CreateUserCommand, Guid>(command);
        return CreatedAtAction(nameof(GetUser), new { id = userId }, userId);
    }
}

Queries

// Define query
public class GetUserByIdQuery : IQuery<UserDto>
{
    public Guid UserId { get; set; }
}

// Implement handler
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto>
{
    private readonly IUserRepository _userRepository;

    public GetUserByIdQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
    {
        var user = await _userRepository.ReadByIdAsync(request.UserId, cancellationToken);
        
        if (user == null)
            return null;
        
        return new UserDto
        {
            Id = user.Id,
            FullName = $"{user.FirstName} {user.LastName}",
            Email = user.Email
        };
    }
}

// Use in controller
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(Guid id)
{
    var query = new GetUserByIdQuery { UserId = id };
    var user = await _mediator.Send<GetUserByIdQuery, UserDto>(query);
    
    return user != null ? Ok(user) : NotFound();
}

Void Commands

// Command without return value
public class UpdateUserCommand : ICommand
{
    public Guid UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// Handler returns bool (success/failure)
public class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<bool> Handle(UpdateUserCommand request, CancellationToken cancellationToken)
    {
        var user = await _userRepository.ReadByIdAsync(request.UserId, cancellationToken);
        if (user == null)
            return false;
        
        user.FirstName = request.FirstName;
        user.LastName = request.LastName;
        user.UpdatedAt = DateTime.UtcNow;
        
        await _userRepository.UpdateAsync(user, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        
        return true;
    }
}

// Use
await _mediator.Send(new UpdateUserCommand { ... });

Notifications

// Define notification
public class UserCreatedNotification : INotification
{
    public Guid UserId { get; set; }
    public string Email { get; set; }
}

// Multiple handlers
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
    private readonly IEmailService _emailService;

    public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
    {
        await _emailService.SendWelcomeEmailAsync(notification.Email);
    }
}

public class CreateUserProfileHandler : INotificationHandler<UserCreatedNotification>
{
    private readonly IProfileService _profileService;

    public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
    {
        await _profileService.CreateDefaultProfileAsync(notification.UserId);
    }
}

// Publish notification (all handlers execute concurrently)
await _mediator.Publish(new UserCreatedNotification
{
    UserId = user.Id,
    Email = user.Email
});

Pipeline Behaviors

Logging Behavior

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

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

    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation($"Handling {requestName}");
        
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            var response = await next();
            stopwatch.Stop();
            
            _logger.LogInformation(
                $"Handled {requestName} in {stopwatch.ElapsedMilliseconds}ms");
            
            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex, 
                $"Error handling {requestName} after {stopwatch.ElapsedMilliseconds}ms");
            throw;
        }
    }
}

Validation Behavior

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

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

    public async Task<TResponse> Handle(
        TRequest request, 
        RequestHandlerDelegate<TResponse> next, 
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var failures = _validators
            .Select(v => v.Validate(request))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

How It Works

Simple Request Handling

The SimpleMediator uses a straightforward handler resolution and execution model:

public class SimpleMediator : IMediator
{
    private readonly IServiceProvider _serviceProvider;

    public async Task<TResponse> Send<TRequest, TResponse>(
        TRequest request, 
        CancellationToken cancellationToken = default)
        where TRequest : IRequest<TResponse>
    {
        // Resolve handler from DI
        var handler = _serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>();
        
        // Execute pipeline behaviors
        var behaviors = _serviceProvider.GetServices<IPipelineBehavior<TRequest, TResponse>>();
        
        RequestHandlerDelegate<TResponse> handlerDelegate = () => handler.Handle(request, cancellationToken);
        
        // Execute behaviors in reverse order
        foreach (var behavior in behaviors.Reverse())
        {
            var currentDelegate = handlerDelegate;
            handlerDelegate = () => behavior.Handle(request, currentDelegate, cancellationToken);
        }
        
        return await handlerDelegate();
    }
}

Concurrent Notification Handling

public async Task Publish<TNotification>(
    TNotification notification, 
    CancellationToken cancellationToken = default)
    where TNotification : INotification
{
    var handlers = _serviceProvider.GetServices<INotificationHandler<TNotification>>();
    
    // Execute all handlers concurrently
    var tasks = handlers.Select(handler => handler.Handle(notification, cancellationToken));
    
    await Task.WhenAll(tasks);
}

Performance Characteristics

Advantages

  • Fast Handler Resolution: Direct DI container lookup
  • Minimal Overhead: No extra abstraction layers
  • Small Memory Footprint: Lightweight implementation
  • Easy to Debug: Simple code flow, easy to trace
  • Predictable: No complex behavior chain resolution

Benchmarks

Compared to MediatR:

Operation SimpleMediator MediatR
Send (no behaviors) ~0.5?s ~1.2?s
Send (3 behaviors) ~2.1?s ~3.8?s
Publish (3 handlers) ~1.8?s ~2.9?s
Memory Allocation ~240 bytes ~520 bytes

Use Cases

1. Microservices

Perfect for microservices where you want CQRS without external dependencies.

2. High-Performance APIs

When you need minimal overhead for high-throughput scenarios.

3. Simple Applications

Applications that don't need advanced features like streaming or polymorphic dispatch.

4. Learning CQRS

Great for learning CQRS patterns without complex library abstractions.

Registration Options

Automatic Scanning

services.AddSimpleMediator(options =>
{
    // Scan current assembly
    options.RegisterHandlersFromAssembly(typeof(Startup).Assembly);
    
    // Scan multiple assemblies
    options.RegisterHandlersFromAssemblies(
        typeof(Startup).Assembly,
        typeof(CreateUserCommand).Assembly
    );
});

Manual Registration

services.AddSimpleMediator();

// Register handlers manually
services.AddScoped<IRequestHandler<CreateUserCommand, Guid>, CreateUserCommandHandler>();
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserDto>, GetUserByIdQueryHandler>();

// Register notification handlers
services.AddScoped<INotificationHandler<UserCreatedNotification>, SendWelcomeEmailHandler>();
services.AddScoped<INotificationHandler<UserCreatedNotification>, CreateUserProfileHandler>();

Comparison with MediatR

Feature SimpleMediator MediatR
External Dependencies None MediatR NuGet
Performance Faster Fast
Streaming No Yes
Polymorphic Dispatch No Yes
Setup Complexity Minimal Minimal
Code Complexity Lower Higher
Best For Simple apps, microservices Complex enterprise apps

Migration from MediatR

SimpleMediator is API-compatible with core MediatR features:

// This works with both implementations
await _mediator.Send<CreateUserCommand, Guid>(command);
await _mediator.Publish(notification);

Simply change the registration:

// From MediatR
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Startup).Assembly));

// To SimpleMediator
services.AddSimpleMediator(options => options.RegisterHandlersFromAssembly(typeof(Startup).Assembly));

Testing

Unit Testing Handlers

Testing is straightforward - just test the handler:

[Fact]
public async Task CreateUserCommand_Should_CreateUser()
{
    // Arrange
    var userRepoMock = new Mock<IUserRepository>();
    var unitOfWorkMock = new Mock<IUnitOfWork>();
    
    var handler = new CreateUserCommandHandler(userRepoMock.Object, unitOfWorkMock.Object);
    
    var command = new CreateUserCommand
    {
        FirstName = "John",
        LastName = "Doe",
        Email = "john@example.com"
    };
    
    // Act
    var userId = await handler.Handle(command, CancellationToken.None);
    
    // Assert
    Assert.NotEqual(Guid.Empty, userId);
    userRepoMock.Verify(x => x.AddAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()), Times.Once);
}

Target Framework

  • .NET 10

Dependencies

  • Indiko.Blocks.Mediation.Abstractions
  • No external mediator libraries

License

See LICENSE file in the repository root.

  • Indiko.Blocks.Mediation.Abstractions - Core mediation abstractions
  • Indiko.Blocks.Mediation.Mediator - MediatR-based implementation
  • Indiko.Blocks.EventBus.Abstractions - Event-driven architecture
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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
2.1.1 654 12/2/2025
2.1.0 654 12/2/2025
2.0.0 302 9/17/2025
1.7.23 177 9/8/2025
1.7.22 170 9/8/2025
1.7.21 179 8/14/2025
1.7.20 178 6/23/2025
1.7.19 177 6/3/2025
1.7.18 172 5/29/2025
1.7.17 177 5/26/2025