Validating Requests with MediatR and FluentValidation in .NET Core
In modern software development, ensuring the validity of incoming requests is crucial. This article will guide you through integrating a validation pipeline using MediatR and FluentValidation in a CQRS (Command Query Responsibility Segregation) pattern. This approach ensures that only valid requests reach your application logic, enhancing efficiency and maintainability.
What is CQRS?
CQRS is a design pattern that separates the read and write operations of your application. Commands change the state of the application, while Queries retrieve data. This separation allows for better scalability, maintainability, and a clear separation of concerns.
Setting Up the Project
To get started, ensure you have a .NET Core project with MediatR and FluentValidation installed. You can install these packages via NuGet:
Install-Package MediatR
Install-Package FluentValidation
Install-Package FluentValidation.DependencyInjectionExtensions
Directory Structure
Organizing your project files properly is essential for maintainability and clarity. Here’s a simplified directory structure for a CQRS Validation Pipeline with MediatR and FluentValidation:
/src
/Application
/Commands
UpdateUserCommand.cs
UpdateUserCommandValidator.cs
/Behaviors
ValidationBehavior.cs
/Controllers
UsersController.cs
/Middlewares
ExceptionHandlingMiddleware.cs
/Program.cs
/Startup.cs
Defining Commands and Queries
In CQRS, commands and queries are represented by classes. Here’s an example of a command to update a user:
public sealed record UpdateUserCommand(int UserId, string FirstName, string LastName) : IRequest<Unit>;
Implementing Validators
FluentValidation allows you to define validation rules for your commands. Here’s how you can create a validator for the UpdateUserCommand
:
public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
{
public UpdateUserCommandValidator()
{
RuleFor(x => x.UserId).NotEmpty();
RuleFor(x => x.FirstName).NotEmpty().MaximumLength(100);
RuleFor(x => x.LastName).NotEmpty().MaximumLength(100);
}
}
Creating a Validation PipelineBehavior
MediatR’s IPipelineBehavior
interface allows you to create middleware-like behaviors that can be executed before and after a request is handled. Here’s how to implement a validation behavior:
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : class, IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators) => _validators = validators;
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (!_validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(request);
var errorsDictionary = _validators
.Select(x => x.Validate(context))
.SelectMany(x => x.Errors)
.Where(x => x != null)
.GroupBy(
x => x.PropertyName,
x => x.ErrorMessage,
(propertyName, errorMessages) => new
{
Key = propertyName,
Values = errorMessages.Distinct().ToArray()
})
.ToDictionary(x => x.Key, x => x.Values);
if (errorsDictionary.Any())
{
throw new ValidationException(errorsDictionary);
}
return await next();
}
}
Handling Validation Exceptions
To handle validation exceptions globally, you can implement a middleware in ASP.NET Core:
internal sealed class ExceptionHandlingMiddleware : IMiddleware
{
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger) => _logger = logger;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
await HandleExceptionAsync(context, e);
}
}
private static async Task HandleExceptionAsync(HttpContext httpContext, Exception exception)
{
var statusCode = GetStatusCode(exception);
var response = new
{
title = GetTitle(exception),
status = statusCode,
detail = exception.Message,
errors = GetErrors(exception)
};
httpContext.Response.ContentType = "application/json";
httpContext.Response.StatusCode = statusCode;
await httpContext.Response.WriteAsync(JsonSerializer.Serialize(response));
}
private static int GetStatusCode(Exception exception) =>
exception switch
{
ValidationException => StatusCodes.Status422UnprocessableEntity,
_ => StatusCodes.Status500InternalServerError
};
private static string GetTitle(Exception exception) =>
exception switch
{
ApplicationException applicationException => applicationException.Title,
_ => "Server Error"
};
private static IReadOnlyDictionary<string, string[]> GetErrors(Exception exception)
{
IReadOnlyDictionary<string, string[]> errors = null;
if (exception is ValidationException validationException)
{
errors = validationException.ErrorsDictionary;
}
return errors;
}
}
Setting Up Dependency Injection
Register the necessary services in your Startup
or Program
class:
services.AddMediatR(typeof(Application.AssemblyReference).Assembly);
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssembly(typeof(Application.AssemblyReference).Assembly);
services.AddTransient<ExceptionHandlingMiddleware>();
Testing the Implementation
Create a controller to test the validation pipeline:
[ApiController]
[Route("api/[controller]")]
public sealed class UsersController : ControllerBase
{
private readonly ISender _sender;
public UsersController(ISender sender) => _sender = sender;
[HttpPut("{userId:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateUser(int userId, [FromBody] UpdateUserRequest request, CancellationToken cancellationToken)
{
var command = request.Adapt<UpdateUserCommand>() with { UserId = userId };
await _sender.Send(command, cancellationToken);
return NoContent();
}
}
Conclusion
By integrating MediatR and FluentValidation into your CQRS pipeline, you can ensure that only valid requests reach your application logic. This approach not only enhances the maintainability and scalability of your application but also provides a clean and efficient way to handle cross-cutting concerns like validation. Including a well-defined directory structure further aids in organizing your project, making it easier to manage and understand.