Validation is an essential cross-cutting concern that you need to solve in your application. You want to ensure the request is valid before you consider processing it.
Another important question you need to answer is how you approach different types of validation. For example, I consider input and business validation differently, and each deserves a specific solution.
I want to show you an elegant solution for validation using MediatR and FluentValidation.
If you aren't using CQRS with MediatR, don't worry. Everything I explain about validation can easily be adapted to other paradigms.
Here's what I'm going to talk about in this week's newsletter:
- Standard validation approach
- Input vs business validation
- Separating validation logic
- Generic
ValidationBehavior
Let's dive in.
The Standard Command Validation Approach
The standard way of implementing validation is right before processing the command. The validation is tightly coupled to the command handler, which could be problematic.
I find this approach difficult to maintain as the complexity of the validation increases. Each change to the validation logic also touches the handler, and the handler itself can grow out of control.
It also makes it harder to differentiate between input and business validation.
Here's an example ShipOrderCommandHandler
that checks if the ShippingAddress.Country
is one of the supported countries:
internal sealed record ShipOrderCommandHandler
: IRequestHandler<ShipOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly IShippingService _shippingService;
private readonly ShipmentSettings _shipmentSettings;
public async Task Handle(
ShipOrderCommand command,
CancellationToken cancellationToken)
{
if (!_shipmentSettings
.SupportedCountries
.Contains(command.ShippingAddress.Country))
{
throw new ArgumentException(nameof(ShipOrderCommand.Address));
}
var order = _orderRepository.Get(command.OrderId);
_shippingService.ShipTo(
command.ShippingAddress,
command.ShippingMethod);
}
}
What if we can separate command validation and command handling?
Input Validation and Business Validation
I mentioned input and business validation in the previous section.
Here's how I consider them to be different:
-
Input validation - We only validate that the command is processable. These are simple validations, such as checking for
null
values, empty strings, etc. - Business validation - We validate the command to satisfy the business rules. This includes checking the system state for required preconditions before processing the command.
Another way to compare them is cheap vs. expensive. Input validation is usually cheap to execute and can be done in memory. While business validation involves reading state and is slower.
So, input validation sits at the entry point of the use case before handling the request. After it completes, we have a valid command. And this is a rule I always follow - an invalid command should never reach the handler.
Input Validation With FluentValidation
FluentValidation is an excellent validation library for .NET, which uses a fluent interface and lambda expressions for building strongly typed validation rules.
Here's the ShipOrderCommand
that we want to validate:
public sealed record ShipOrderCommand : IRequest
{
public Guid OrderId { get; set; }
public string ShippingMethod { get; set; }
public Address ShippingAddress { get; set; }
}
To implement a validator with FluentValidation, you create a class that inherits from the AbstractValidator<T>
base class. Then, you can add the validation rules from the constructor using RuleFor
:
public sealed record ShipOrderCommandValidator
: AbstractValidator<ShipOrderCommand>
{
public ShipOrderCommandValidator(ShipmentSettings settings)
{
RuleFor(command => command.OrderId)
.NotEmpty()
.WithMessage("The order identifier can't be empty.");
RuleFor(command => command.ShippingMethod)
.NotEmpty()
.WithMessage("The shipping method can't be empty.");
RuleFor(command => command.ShippingAddress)
.NotNull()
.WithMessage("The shipping address can't be empty.");
RuleFor(command => command.ShippingAddress.Country)
.Must(country => settings.SupportedCountries.Contains(country))
.WithMessage("The shipping country isn't supported.");
}
}
The naming convention I like to use is the name of the command and append Validator. You can also enforce this by writing architecture tests.
To automatically register all validators from an assembly, you need to call the AddValidatorsFromAssembly
method:
services.AddValidatorsFromAssembly(ApplicationAssembly.Assembly);
Running Validation From the Use Case
To run the ShipOrderCommandValidator
, you can use the IValidator<T>
service and inject it from the constructor.
The validator exposes a few methods you can call, like Validate
, ValidateAsync
, or ValidateAndThrow
.
The Validate
method returns a ValidationResult
object which contains two properties:
-
IsValid
- a boolean flag saying whether the validation succeeded -
Errors
- a collection ofValidationFailure
objects containing any validation failures
Alternatively, calling the ValidateAndThrow
method throws a ValidationException
if validation fails.
internal sealed record ShipOrderCommandHandler
: IRequestHandler<ShipOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly IShippingService _shippingService;
private readonly IValidator<ShipOrderCommand> _validator;
public async Task Handle(
ShipOrderCommand command,
CancellationToken cancellationToken)
{
_validator.ValidateAndThrow(command);
var order = _orderRepository.Get(command.OrderId);
_shippingService.ShipTo(
command.ShippingAddress,
command.ShippingMethod);
}
}
This approach forces you to define an explicit dependency on IValidator
in every command handler.
What if we can implement this cross-cutting concern in a more generic way?
MediatR Validation Pipeline
Here's a complete implementation of a ValidationBehavior
using FluentValidation and MediatR's IPipelineBehavior
.
The ValidationBehavior
acts as a middleware for the request pipeline and performs validation. If the validation fails, it will throw a custom ValidationException
with a collection of ValidationError
objects.
I also want to highlight the use of ValidateAsync
, which allows you to define asynchronous validation rules. You must call the ValidateAsync
method if you have asynchronous rules. Otherwise, the validator will throw an exception.
public class ValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommandBase
{
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)
{
var context = new ValidationContext<TRequest>(request);
var validationFailures = await Task.WhenAll(
_validators.Select(validator => validator.ValidateAsync(context)));
var errors = validationFailures
.Where(validationResult => !validationResult.IsValid)
.SelectMany(validationResult => validationResult.Errors)
.Select(validationFailure => new ValidationError(
validationFailure.PropertyName,
validationFailure.ErrorMessage))
.ToList();
if (errors.Any())
{
throw new Exceptions.ValidationException(errors);
}
var response = await next();
return response;
}
}
Don't forget to register the ValidationBehavior
with MediatR by calling AddOpenBehavior
:
services.AddMediatR(config =>
{
config.RegisterServicesFromAssemblyContaining<ApplicationAssembly>();
config.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
Handling Validation Exceptions
Here's a custom ValidationExceptionHandlingMiddleware
middleware that only handles the custom ValidationException
. It converts the exception to a ProblemDetails
response and includes any validation errors.
You can easily expand this to be a generic global exception handler.
public class ValidationExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ValidationExceptionHandlingMiddleware(RequestDelegate next)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exceptions.ValidationException exception)
{
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Type = "ValidationFailure",
Title = "Validation error",
Detail = "One or more validation errors has occurred"
};
if (exception.Errors is not null)
{
problemDetails.Extensions["errors"] = exception.Errors;
}
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(problemDetails);
}
}
}
You also need to include the middleware in the request pipeline by calling UseMiddleware
:
app.UseMiddleware<ExceptionHandlingMiddleware>();
Takeaway
This implementation of ValidationBehavior
is something I use in real projects, and it works incredibly well. If I don't want to throw an exception, I can update the ValidationBehavior
to return a result object instead.
How do you apply this if you're not using MediatR?
I'm using an IPipelineBehavior
, which allows me to implement a middleware wrapping each request.
So, all you need is a way to implement middleware and place your validation inside. And I like having options, so here are three ways to create middleware in ASP.NET Core.
Hope this was valuable.
Stay awesome!
P.S. Whenever you're ready, there are 2 ways I can help you:
Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 1,000+ students here.
Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 860+ engineers here.
Top comments (0)