In a CQRS-based architecture, the command (Command) represents the intention to change the application's state. Therefore, it is essential that these commands are rigorously validated before they reach the handler that processes them.
At this stage, we will integrate FluentValidation to validate the commands sent to MediatR, particularly CreatePostCommand.
✅ Why FluentValidation?
-
Clear separation of validation rules from business logic.
-
Full support for validating complex objects.
-
Automatic integration with IRequest<T> from MediatR.
-
Fluent, readable, and extensible validations.
🛠️ 1. Installing FluentValidation
Add to the Application project:
dotnet add package FluentValidation
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
📦 2. Create CreatePostCommand
public record CreatePostCommand(string Title, string Content) : IRequest<Guid>;
🧪 3. Add CreatePostCommandValidator
using FluentValidation;
public class CreatePostCommandValidator : AbstractValidator<CreatePostCommand>
{
public CreatePostCommandValidator()
{
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required")
.MaximumLength(100).WithMessage("Title cannot exceed 100 characters");
RuleFor(x => x.Content)
.NotEmpty().WithMessage("Content is required")
.MinimumLength(100).WithMessage("Content must be at least 100 characters");
}
}
🧩 4. Integrating validation with MediatR
Create a ValidationBehavior:
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
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 failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
Registration in DI:
services.AddValidatorsFromAssemblyContaining<CreatePostCommandValidator>();
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
🧪 5. Test example
var validator = new CreatePostCommandValidator();
var result = validator.Validate(new CreatePostCommand("", "Abc"));
Console.WriteLine(result.IsValid); // False
🧵 Conclusions
✅ By applying FluentValidation only on Commands, we ensure that strict validations are separated from reads. This pattern respects the Single Responsibility principle and prepares the ground for scaling and testing.