Overview
The Intent.Application.MediatR.FluentValidation module integrates FluentValidation into your MediatR pipeline, automatically validating commands and queries before they reach their handlers. It generates validators for your requests and a pipeline behaviour that executes them.
FluentValidation provides a fluent interface for building strongly-typed validation rules, making your validation logic clean, testable, and reusable.
What Gets Generated
This module generates:
ValidationBehaviour - Pipeline behaviour that runs validators before handlers
Command Validators - AbstractValidator<TCommand> for each command
Query Validators - AbstractValidator<TQuery> for each query
Generated ValidationBehaviour
The validation behaviour intercepts all requests and runs their validators:
public class ValidationBehaviour < TRequest , TResponse > : IPipelineBehavior < TRequest , TResponse >
where TRequest : notnull
{
private readonly IEnumerable < IValidator < TRequest >> _validators ;
public ValidationBehaviour ( IEnumerable < IValidator < TRequest >> validators )
{
_validators = validators ;
}
public async Task < TResponse > Handle (
TRequest request ,
RequestHandlerDelegate < TResponse > next ,
CancellationToken cancellationToken )
{
if ( _validators . Any ())
{
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 )
{
throw new ValidationException ( failures );
}
}
return await next ();
}
}
Key features:
Runs all validators for the request type
Executes validators in parallel for performance
Throws ValidationException with all failures if validation fails
Registered with priority 4 in the MediatR pipeline (after authorization, before unit of work)
Generated Validator Example
Given a CreateCustomerCommand in the Services Designer:
CreateCustomerCommandValidator.cs
[ IntentManaged ( Mode . Fully , Body = Mode . Merge )]
public class CreateCustomerCommandValidator : AbstractValidator < CreateCustomerCommand >
{
[ IntentManaged ( Mode . Merge )]
public CreateCustomerCommandValidator ()
{
ConfigureValidationRules ();
}
private void ConfigureValidationRules ()
{
RuleFor ( v => v . Name )
. NotNull ()
. MaximumLength ( 100 );
RuleFor ( v => v . Surname )
. NotNull ()
. MaximumLength ( 100 );
RuleFor ( v => v . Email )
. NotNull ()
. EmailAddress ()
. MaximumLength ( 255 );
}
}
Validation Rules
The module generates validation rules based on:
Type-Based Rules
String Properties
NotNull() for required strings
MaximumLength() from designer metadata
EmailAddress() for email types
Numeric Properties
NotNull() for non-nullable numbers
GreaterThan() / LessThan() from validation stereotypes
Range validations
Collections
NotNull() for required collections
NotEmpty() when specified
Child validator for collection items
Complex Types
SetValidator() for nested DTOs
Reuses existing DTO validators
Validates object graphs
Designer-Driven Rules
You can specify validation rules in the Services Designer using Validation stereotypes:
Property: Age
Validation:
- Min Value: 18
- Max Value: 120
- Error Message: "Age must be between 18 and 120"
Generates:
RuleFor ( v => v . Age )
. GreaterThanOrEqualTo ( 18 )
. LessThanOrEqualTo ( 120 )
. WithMessage ( "Age must be between 18 and 120" );
Advanced Validation Features
Unique Constraint Validation
When enabled, the module can validate uniqueness against the database:
RuleFor ( v => v . Email )
. MustAsync ( async ( email , cancellation ) =>
{
return ! await _customerRepository . AnyAsync ( x => x . Email == email , cancellation );
})
. WithMessage ( "Email '{PropertyValue}' already exists." );
FluentValidation Application Layer.Unique Constraint Validation
Enables generation of unique constraint validation rules that query the repository. Requires: Repository injection in validators
Custom Validation Methods
Validators support custom validation logic:
CreateCustomerCommandValidator.cs
[ IntentManaged ( Mode . Fully , Body = Mode . Merge )]
public class CreateCustomerCommandValidator : AbstractValidator < CreateCustomerCommand >
{
[ IntentManaged ( Mode . Merge )]
public CreateCustomerCommandValidator ()
{
ConfigureValidationRules ();
}
[ IntentManaged ( Mode . Fully )]
private void ConfigureValidationRules ()
{
// Auto-generated rules
RuleFor ( v => v . Email )
. NotNull ()
. EmailAddress ();
}
[ IntentManaged ( Mode . Ignore )]
private void CustomValidation ()
{
// Your custom rules
RuleFor ( v => v . Email )
. Must ( email => ! email . EndsWith ( ".test" ))
. WithMessage ( "Test emails are not allowed" );
}
}
Conditional Validation
FluentValidation supports conditional rules:
RuleFor ( v => v . CompanyName )
. NotNull ()
. When ( v => v . CustomerType == CustomerType . Business );
RuleFor ( v => v . TaxId )
. NotNull ()
. When ( v => v . CustomerType == CustomerType . Business )
. WithMessage ( "Tax ID is required for business customers" );
Dependent Validation
Validate one property based on another:
RuleFor ( v => v . EndDate )
. GreaterThan ( v => v . StartDate )
. WithMessage ( "End date must be after start date" );
Collection Validation
Validate items in collections:
RuleFor ( v => v . OrderLines )
. NotEmpty ()
. WithMessage ( "Order must have at least one line item" );
RuleForEach ( v => v . OrderLines )
. SetValidator ( new OrderLineValidator ());
Validator Reuse
Validators for DTOs are automatically reused:
public class CreateCustomerCommandValidator : AbstractValidator < CreateCustomerCommand >
{
public CreateCustomerCommandValidator ( IValidator < AddressDto > addressValidator )
{
// Reuses existing AddressDto validator
RuleFor ( v => v . Address )
. SetValidator ( addressValidator );
}
}
Benefits:
DRY principle - validation rules defined once
Consistent validation across DTOs
Easier maintenance
Validation Error Handling
When validation fails, ValidationException is thrown with all errors:
try
{
await _mediator . Send ( new CreateCustomerCommand ( .. .));
}
catch ( ValidationException ex )
{
foreach ( var error in ex . Errors )
{
Console . WriteLine ( $" { error . PropertyName } : { error . ErrorMessage } " );
}
}
Error structure:
{
"errors" : [
{
"propertyName" : "Email" ,
"errorMessage" : "'Email' is not a valid email address." ,
"attemptedValue" : "invalid-email" ,
"severity" : "Error"
},
{
"propertyName" : "Name" ,
"errorMessage" : "'Name' must not be empty." ,
"attemptedValue" : null ,
"severity" : "Error"
}
]
}
File Consolidation
When Consolidate Command/Query associated files is enabled in CQRS Settings:
// Command, Handler, and Validator all in one file
public class CreateCustomerCommand : IRequest < Guid >, ICommand
{
// Command properties...
}
public class CreateCustomerCommandHandler : IRequestHandler < CreateCustomerCommand , Guid >
{
// Handler implementation...
}
public class CreateCustomerCommandValidator : AbstractValidator < CreateCustomerCommand >
{
// Validator rules...
}
Configuration
Unique Constraint Validation
FluentValidation Application Layer.Unique Constraint Validation
When enabled, generates database uniqueness checks for properties marked with unique constraints. Requires:
Repository injection enabled
Unique constraint stereotypes in designer
Repository Injection
repositoryInjectionEnabled
Allows validators to inject repositories for async validation against the database. Example: public CreateCustomerCommandValidator ( ICustomerRepository repository )
{
_repository = repository ;
ConfigureValidationRules ();
}
Dependencies
This module requires:
Intent.Application.MediatR - Base CQRS implementation
Intent.Application.FluentValidation - Base FluentValidation setup
Intent.Modelers.Services.CQRS - CQRS modeling designer
Optional dependencies:
Intent.Entities.Constants - For entity constant references in validation
Behaviour Pipeline Position
The ValidationBehaviour is registered with priority 4 in the pipeline:
1. Logging (0)
2. Performance (1)
3. Authorization (2)
4. Validation (4) ← ValidationBehaviour
5. UnitOfWork (5)
6. Handler
Validation runs after authorization but before the unit of work, ensuring invalid requests are rejected before database transactions begin.
Testing Validators
FluentValidation validators are easy to unit test:
CreateCustomerCommandValidatorTests.cs
public class CreateCustomerCommandValidatorTests
{
private readonly CreateCustomerCommandValidator _validator ;
public CreateCustomerCommandValidatorTests ()
{
_validator = new CreateCustomerCommandValidator ();
}
[ Fact ]
public void Should_Have_Error_When_Name_Is_Empty ()
{
var command = new CreateCustomerCommand (
name : "" ,
surname : "Doe" ,
email : "[email protected] " );
var result = _validator . TestValidate ( command );
result . ShouldHaveValidationErrorFor ( x => x . Name );
}
[ Fact ]
public void Should_Not_Have_Error_When_Command_Is_Valid ()
{
var command = new CreateCustomerCommand (
name : "John" ,
surname : "Doe" ,
email : "[email protected] " );
var result = _validator . TestValidate ( command );
result . ShouldNotHaveAnyValidationErrors ();
}
}
MediatR Core CQRS implementation
MediatR Behaviours Other pipeline behaviours (logging, authorization, etc.)
FluentValidation for DTOs Validation for application DTOs
Additional Resources