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
<PackageReference Include="Indiko.Blocks.Mediation.SimpleMediator" Version="2.1.1" />
<PackageVersion Include="Indiko.Blocks.Mediation.SimpleMediator" Version="2.1.1" />
<PackageReference Include="Indiko.Blocks.Mediation.SimpleMediator" />
paket add Indiko.Blocks.Mediation.SimpleMediator --version 2.1.1
#r "nuget: Indiko.Blocks.Mediation.SimpleMediator, 2.1.1"
#:package Indiko.Blocks.Mediation.SimpleMediator@2.1.1
#addin nuget:?package=Indiko.Blocks.Mediation.SimpleMediator&version=2.1.1
#tool nuget:?package=Indiko.Blocks.Mediation.SimpleMediator&version=2.1.1
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.
Related Packages
Indiko.Blocks.Mediation.Abstractions- Core mediation abstractionsIndiko.Blocks.Mediation.Mediator- MediatR-based implementationIndiko.Blocks.EventBus.Abstractions- Event-driven architecture
| Product | Versions 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. |
-
net10.0
- Indiko.Blocks.Mediation.Abstractions (>= 2.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.