A lightweight and efficient implementation of the Mediator pattern for .NET 8 applications. SimpleMediator provides a clean abstraction for handling commands, queries, and notifications while maintaining simplicity and performance.
Note: This library is designed as an educational implementation and is not intended as a commercial solution.
- Request/Response Pattern: Handle commands and queries with typed responses
- Notifications: Event-driven architecture with one-to-many communication
- Stream Requests: Support for asynchronous data streams using
IAsyncEnumerable - Dependency Injection: Seamless integration with Microsoft.Extensions.DependencyInjection
- Auto Registration: Automatic discovery and registration of handlers from assemblies
- Custom Behaviors: Extensible pipeline for cross-cutting concerns
- .NET 8 Native: Built specifically for .NET 8 with modern C# features
Add the package reference to your project:
dotnet add package Nast.SimpleMediatorRegister SimpleMediator in your dependency injection container:
using Nast.SimpleMediator;
var builder = WebApplication.CreateBuilder(args);
// Basic registration - scans all loaded assemblies
builder.Services.AddMediator();
// Register specific assemblies
builder.Services.AddMediator(typeof(Program).Assembly);
// Advanced configuration
builder.Services.AddMediator(options =>
{
options.RegisterServicesFromAssemblies(typeof(Program).Assembly);
options.AddBehavior<ILoggingBehavior, LoggingBehavior>();
});
var app = builder.Build();SimpleMediator supports both commands (requests without responses) and queries (requests with responses).
// Query with response
public record GetUserQuery(int UserId) : IRequest<User>;
// Command without response
public record CreateUserCommand(string Name, string Email) : IRequest;public class GetUserHandler : IRequestHandler<GetUserQuery, User>
{
private readonly IUserRepository _repository;
public GetUserHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<User> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.UserId);
}
}
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
public CreateUserHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User(request.Name, request.Email);
await _repository.AddAsync(user);
}
}[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("{id}")]
public async Task<User> GetUser(int id)
{
return await _mediator.Send(new GetUserQuery(id));
}
[HttpPost]
public async Task CreateUser(CreateUserCommand command)
{
await _mediator.Send(command);
}
}Notifications allow decoupled event-driven communication where multiple handlers can respond to a single event.
public record UserCreatedNotification(int UserId, string Email) : INotification;public class EmailNotificationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IEmailService _emailService;
public EmailNotificationHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
await _emailService.SendWelcomeEmailAsync(notification.Email);
}
}
public class LoggingNotificationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly ILogger<LoggingNotificationHandler> _logger;
public LoggingNotificationHandler(ILogger<LoggingNotificationHandler> logger)
{
_logger = logger;
}
public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation("User {UserId} was created", notification.UserId);
}
}public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
private readonly IMediator _mediator;
public CreateUserHandler(IUserRepository repository, IMediator mediator)
{
_repository = repository;
_mediator = mediator;
}
public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User(request.Name, request.Email);
await _repository.AddAsync(user);
// Publish notification - all handlers will be executed
await _mediator.Publish(new UserCreatedNotification(user.Id, user.Email));
}
}Stream requests enable asynchronous data streaming using IAsyncEnumerable<T>.
public record GetUsersStreamQuery(string Filter) : IStreamRequest<User>;public class GetUsersStreamHandler : IStreamRequestHandler<GetUsersStreamQuery, User>
{
private readonly IUserRepository _repository;
public GetUsersStreamHandler(IUserRepository repository)
{
_repository = repository;
}
public async IAsyncEnumerable<User> Handle(
GetUsersStreamQuery request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var user in _repository.GetFilteredUsersAsync(request.Filter))
{
cancellationToken.ThrowIfCancellationRequested();
yield return user;
}
}
}[HttpGet("stream")]
public async IAsyncEnumerable<User> GetUsersStream(string filter)
{
await foreach (var user in _mediator.CreateStream(new GetUsersStreamQuery(filter)))
{
yield return user;
}
}SimpleMediator provides several convenience extension methods:
// Send multiple notifications
var notifications = new List<INotification>
{
new UserCreatedNotification(1, "user1@example.com"),
new UserCreatedNotification(2, "user2@example.com")
};
await _mediator.PublishAll(notifications);
// Use publisher directly
await _publisher.PublishAll(notifications);IMediator: Primary interface combiningISenderandIPublisherISender: Handles request/response operations and stream creationIPublisher: Manages notification publishing
IRequest<TResponse>: Request expecting a typed responseIRequest: Request without response (command pattern)IStreamRequest<TResponse>: Request for streaming data
IRequestHandler<TRequest, TResponse>: Handles requests with responsesIRequestHandler<TRequest>: Handles requests without responsesINotificationHandler<TNotification>: Handles notificationsIStreamRequestHandler<TRequest, TResponse>: Handles stream requests
You can extend SimpleMediator with custom behaviors for cross-cutting concerns:
public interface ILoggingBehavior
{
Task ExecuteAsync(Func<Task> next);
}
public class LoggingBehavior : ILoggingBehavior
{
private readonly ILogger<LoggingBehavior> _logger;
public LoggingBehavior(ILogger<LoggingBehavior> logger)
{
_logger = logger;
}
public async Task ExecuteAsync(Func<Task> next)
{
_logger.LogInformation("Executing operation");
await next();
_logger.LogInformation("Operation completed");
}
}
// Register behavior
builder.Services.AddMediator(options =>
{
options.AddBehavior<ILoggingBehavior, LoggingBehavior>();
});- .NET 8.0 or higher
- Microsoft.Extensions.DependencyInjection.Abstractions 9.0.7
SimpleMediator follows a clean architecture approach with clear separation of concerns:
- Abstractions: Core interfaces defining contracts
- Internal: Implementation details hidden from consumers
- Extensions: Convenience methods for common operations
- Registration: Dependency injection configuration
This project is licensed under the MIT License - see the LICENSE file for details.