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
                    
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="CG.Infrastructure.Mediator" Version="3.10.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CG.Infrastructure.Mediator" Version="3.10.6" />
                    
Directory.Packages.props
<PackageReference Include="CG.Infrastructure.Mediator" />
                    
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 CG.Infrastructure.Mediator --version 3.10.6
                    
#r "nuget: CG.Infrastructure.Mediator, 3.10.6"
                    
#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 CG.Infrastructure.Mediator@3.10.6
                    
#: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=CG.Infrastructure.Mediator&version=3.10.6
                    
Install as a Cake Addin
#tool nuget:?package=CG.Infrastructure.Mediator&version=3.10.6
                    
Install as a Cake Tool

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

  1. Single Responsibility: Each handler should handle only one type of request
  2. Validation: Always validate requests using FluentValidation
  3. Logging: Include meaningful log messages for debugging and monitoring
  4. Error Handling: Use appropriate exception types and provide meaningful error messages
  5. Async Operations: Always use async/await for I/O operations
  6. Cancellation Tokens: Pass cancellation tokens through the pipeline for proper cancellation support
  7. Dependency Injection: Use constructor injection for dependencies
  8. Testing: Write unit tests for handlers and validators separately
  9. 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 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. 
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
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