Skip to main content

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:
ValidationBehaviour.cs
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
boolean
default:"true"
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:
CreateCustomerCommand.cs
// 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
boolean
default:"true"
When enabled, generates database uniqueness checks for properties marked with unique constraints.Requires:
  • Repository injection enabled
  • Unique constraint stereotypes in designer

Repository Injection

repositoryInjectionEnabled
boolean
default:"true"
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

Build docs developers (and LLMs) love