CG.Infrastructure.Mediator
3.10.6
dotnet add package CG.Infrastructure.Mediator --version 3.10.6
NuGet\Install-Package CG.Infrastructure.Mediator -Version 3.10.6
<PackageReference Include="CG.Infrastructure.Mediator" Version="3.10.6" />
<PackageVersion Include="CG.Infrastructure.Mediator" Version="3.10.6" />
<PackageReference Include="CG.Infrastructure.Mediator" />
paket add CG.Infrastructure.Mediator --version 3.10.6
#r "nuget: CG.Infrastructure.Mediator, 3.10.6"
#:package CG.Infrastructure.Mediator@3.10.6
#addin nuget:?package=CG.Infrastructure.Mediator&version=3.10.6
#tool nuget:?package=CG.Infrastructure.Mediator&version=3.10.6
Infrastructure.Mediator
A .NET library that provides a clean implementation of the Mediator pattern using MediatR, along with validation support through FluentValidation and seamless integration with the infrastructure ecosystem.
Overview
Infrastructure.Mediator is a .NET 9.0 library that implements the Mediator pattern to decouple components in your application. It provides a clean, maintainable way to handle requests, commands, and queries while maintaining separation of concerns and enabling easy testing and validation.
Features
- Mediator Pattern Implementation: Clean separation between request/response handling and business logic
- Request/Response Pipeline: Built-in support for commands, queries, and notifications
- Validation Integration: Seamless integration with FluentValidation for request validation
- Dependency Injection: Full support for .NET dependency injection
- Logging Support: Built-in logging abstractions for request/response tracking
- Database Context Integration: Seamless integration with data access layers using query constants
- Service Integration: Leverages the core infrastructure services
- Event Sourcing Support: Built-in support for event sourcing with StoredEvents
Target Framework
- .NET 9.0
Dependencies
Core Dependencies
- CG.Infrastructure.Core (3.10.8) - Core infrastructure components and service extensions
- CG.Infrastructure.Data (3.10.7) - Data access layer and database context support
- CG.Infrastructure.Services (3.10.6) - Service layer abstractions and implementations
Mediator Framework
- MediatR (13.0.0) - Implementation of the mediator pattern with support for CQRS
Validation
- FluentValidation (12.0.0) - Fluent validation library for request validation
Logging
- Microsoft.Extensions.Logging.Abstractions (9.0.8) - Logging abstractions for .NET
Package Information
- Package ID: CG.Infrastructure.Mediator
- Version: 3.10.5
- Authors: Matthew Evans
- Company: Matthew Evans
- Product: CG.Infrastructure.Mediator
- Description: Infra Mediator library with MediatR setup, extensions and database contexts
Architecture
Query Constants Pattern
The library uses a query constants pattern for database operations, similar to other infrastructure projects:
public static class StoredEventQueries
{
public const string GetAll = @"
SELECT [Id], [MessageType], [AggregateId], [Timestamp], [Data], [User]
FROM [dbo].[StoredEvents]
ORDER BY [Timestamp] DESC";
public const string Create = @"
INSERT INTO [dbo].[StoredEvents] ([Id], [MessageType], [AggregateId], [Timestamp], [Data], [User])
VALUES (@Id, @MessageType, @AggregateId, @Timestamp, @Data, @User)";
}
Repository Pattern
Repositories use the query constants directly with the data access layer:
public class EventRepository : IEventRepository
{
public async Task<IEnumerable<StoredEvent>> GetAllAsync()
{
return await dataAccess.LoadData<StoredEvent, object>(
StoredEventQueries.GetAll,
new { },
mediatorOptions.ConnectionName,
CommandType.Text);
}
}
Getting Started
Installation
dotnet add package CG.Infrastructure.Mediator
Basic Setup
// Program.cs or Startup.cs
using MediatR;
using FluentValidation;
var builder = WebApplication.CreateBuilder(args);
// Add MediatR
builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
// Add FluentValidation
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
Usage Examples
Creating a Command
public class CreateUserCommand : IRequest<int>
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.FirstName).NotEmpty().MaximumLength(50);
RuleFor(x => x.LastName).NotEmpty().MaximumLength(50);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly ILogger<CreateUserCommandHandler> _logger;
private readonly IUserRepository _userRepository;
public CreateUserCommandHandler(ILogger<CreateUserCommandHandler> logger, IUserRepository userRepository)
{
_logger = logger;
_userRepository = userRepository;
}
public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Creating user with email: {Email}", request.Email);
var user = new User
{
FirstName = request.FirstName,
LastName = request.LastName,
Email = request.Email
};
var userId = await _userRepository.CreateAsync(user, cancellationToken);
_logger.LogInformation("User created successfully with ID: {UserId}", userId);
return userId;
}
}
Creating a Query
public class GetUserByIdQuery : IRequest<User?>
{
public int UserId { get; set; }
}
public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, User?>
{
private readonly ILogger<GetUserByIdQueryHandler> _logger;
private readonly IUserRepository _userRepository;
public GetUserByIdQueryHandler(ILogger<GetUserByIdQueryHandler> logger, IUserRepository userRepository)
{
_logger = logger;
_userRepository = userRepository;
}
public async Task<User?> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
_logger.LogInformation("Retrieving user with ID: {UserId}", request.UserId);
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
if (user == null)
{
_logger.LogWarning("User with ID {UserId} not found", request.UserId);
}
return user;
}
}
Using in Controllers
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<ActionResult<int>> CreateUser(CreateUserCommand command)
{
var userId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetUser), new { id = userId }, userId);
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
var query = new GetUserByIdQuery { UserId = id };
var user = await _mediator.Send(query);
if (user == null)
return NotFound();
return Ok(user);
}
}
Notifications
public class UserCreatedNotification : INotification
{
public int UserId { get; set; }
public string Email { get; set; } = string.Empty;
}
public class UserCreatedNotificationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly ILogger<UserCreatedNotificationHandler> _logger;
private readonly IEmailService _emailService;
public UserCreatedNotificationHandler(ILogger<UserCreatedNotificationHandler> logger, IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Sending welcome email to user {UserId}", notification.UserId);
await _emailService.SendWelcomeEmailAsync(notification.Email, cancellationToken);
_logger.LogInformation("Welcome email sent successfully to user {UserId}", notification.UserId);
}
}
Advanced Features
Pipeline Behaviors
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
private readonly ILogger<ValidationBehavior<TRequest, TResponse>> _logger;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidationBehavior<TRequest, TResponse>> logger)
{
_validators = validators;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
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)
{
_logger.LogWarning("Validation failed for request {RequestType}: {Failures}", typeof(TRequest).Name, failures);
throw new ValidationException(failures);
}
return await next();
}
}
// Register the behavior
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
Logging Pipeline 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)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
_logger.LogInformation("Handled {RequestType} in {ElapsedMilliseconds}ms", typeof(TRequest).Name, sw.ElapsedMilliseconds);
return response;
}
}
Best Practices
- Single Responsibility: Each handler should handle only one type of request
- Validation: Always validate requests using FluentValidation
- Logging: Include meaningful log messages for debugging and monitoring
- Error Handling: Use appropriate exception types and provide meaningful error messages
- Async Operations: Always use async/await for I/O operations
- Cancellation Tokens: Pass cancellation tokens through the pipeline for proper cancellation support
- Dependency Injection: Use constructor injection for dependencies
- Testing: Write unit tests for handlers and validators separately
- Query Constants: Use the query constants pattern for database operations instead of stored procedures
Testing
Handler Testing
[Test]
public async Task Handle_ValidCommand_ReturnsUserId()
{
// Arrange
var command = new CreateUserCommand { FirstName = "John", LastName = "Doe", Email = "john@example.com" };
var mockRepository = new Mock<IUserRepository>();
var mockLogger = new Mock<ILogger<CreateUserCommandHandler>>();
mockRepository.Setup(x => x.CreateAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
var handler = new CreateUserCommandHandler(mockLogger.Object, mockRepository.Object);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
Assert.That(result, Is.EqualTo(1));
mockRepository.Verify(x => x.CreateAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()), Times.Once);
}
Validator Testing
[Test]
public void Validate_EmptyFirstName_ReturnsValidationError()
{
// Arrange
var command = new CreateUserCommand { FirstName = "", LastName = "Doe", Email = "john@example.com" };
var validator = new CreateUserCommandValidator();
// Act
var result = validator.Validate(command);
// Assert
Assert.That(result.IsValid, Is.False);
Assert.That(result.Errors.Any(e => e.PropertyName == nameof(CreateUserCommand.FirstName)), Is.True);
}
Contributing
This project is part of the CG Infrastructure Libraries. For contributions, please refer to the main infrastructure repository guidelines.
License
See the LICENSE file in the root directory for licensing information.
Support
For issues and questions related to this library, please contact the development team or create an issue in the project repository.
Product | Versions 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. |
-
net9.0
- CG.Infrastructure.Core (>= 3.10.8)
- CG.Infrastructure.Data (>= 3.10.7)
- CG.Infrastructure.Services (>= 3.10.6)
- FluentValidation (>= 12.0.0)
- MediatR (>= 13.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.8)
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 |
---|---|---|
3.10.6 | 140 | 8/18/2025 |
3.10.5 | 67 | 8/16/2025 |
3.10.4 | 104 | 8/15/2025 |
3.10.3 | 134 | 8/10/2025 |
3.10.2 | 162 | 6/19/2025 |
3.10.1 | 153 | 6/18/2025 |
3.10.0 | 163 | 6/16/2025 |
3.9.0 | 144 | 12/10/2024 |
3.0.3 | 170 | 8/13/2024 |
3.0.2 | 159 | 8/9/2024 |
3.0.1 | 154 | 7/16/2024 |
3.0.0 | 151 | 7/16/2024 |
2.0.1 | 806 | 5/16/2023 |
2.0.0 | 696 | 5/16/2023 |
1.0.0 | 801 | 5/26/2022 |