Skip to main content

Overview

The Intent.Application.FluentValidation module provides comprehensive validation support for application services using the FluentValidation library. Apply validation rules declaratively in the designer, and the module generates fully-configured validators.
Module: Intent.Application.FluentValidationVersion: 3.11.6+Dependencies:
  • Intent.Common.CSharp
  • Intent.Modelers.Services
NuGet Package: FluentValidation.DependencyInjectionExtensions v12.x (automatically installed)

Key Features

  • Designer-Driven Validation: Define rules in the Services Designer
  • Rich Validation Rules: 20+ built-in validation types
  • Custom Validators: Add custom validation logic
  • Automatic DI Registration: Validators registered automatically
  • MediatR Integration: Pipeline validation for commands/queries
  • Unique Constraint Validation: Database-backed uniqueness checks
  • Conditional Validation: Apply rules based on conditions
  • Cascade Modes: Control validation behavior
  • Error Message Customization: Override default messages

Installation

intent install Intent.Application.FluentValidation
For DTO validation, also install:
intent install Intent.Application.FluentValidation.Dtos

Quick Start

1

Apply Validation Stereotype

In the Services Designer, apply the Validation stereotype to a parameter or DTO field
2

Configure Rules

Select validation rules (Required, Min Length, Max Length, etc.)
3

Generate Code

Run the Software Factory to generate validator classes
4

Automatic Validation

Validation runs automatically before service execution

Validation Rules

String Validations

Ensures the value is not null or empty.
Generated Validator
RuleFor(x => x.Name)
    .NotEmpty()
    .WithMessage("'Name' is required.");
Minimum string length.
Generated Validator
RuleFor(x => x.Description)
    .MinimumLength(10)
    .WithMessage("'Description' must be at least 10 characters.");
Maximum string length.
Generated Validator
RuleFor(x => x.Name)
    .MaximumLength(100)
    .WithMessage("'Name' must not exceed 100 characters.");
Validates email format.
Generated Validator
RuleFor(x => x.Email)
    .EmailAddress()
    .WithMessage("'Email' is not a valid email address.");
Custom regex pattern validation.
Generated Validator
RuleFor(x => x.PhoneNumber)
    .Matches(@"^\+?[1-9]\d{1,14}$")
    .WithMessage("'Phone Number' must be a valid phone number.")
    .WithTimeout(TimeSpan.FromMilliseconds(100));

Numeric Validations

Minimum numeric value.
Generated Validator
RuleFor(x => x.Age)
    .GreaterThanOrEqualTo(0)
    .WithMessage("'Age' must be 0 or greater.");
Maximum numeric value.
Generated Validator
RuleFor(x => x.Quantity)
    .LessThanOrEqualTo(1000)
    .WithMessage("'Quantity' must not exceed 1000.");
Value must be within a range.
Generated Validator
RuleFor(x => x.Price)
    .InclusiveBetween(0.01m, 999999.99m)
    .WithMessage("'Price' must be between 0.01 and 999999.99.");

Collection Validations

Collection must contain at least one item.
Generated Validator
RuleFor(x => x.Items)
    .NotEmpty()
    .WithMessage("'Items' must not be empty.");
Validate each item in a collection.
Generated Validator
RuleForEach(x => x.Items)
    .SetValidator(new OrderItemValidator());

Special Validations

Value must be a valid enum value.
Generated Validator
RuleFor(x => x.Status)
    .IsInEnum()
    .WithMessage("'Status' must be a valid status value.");

// For collections
RuleForEach(x => x.Statuses)
    .IsInEnum();
Custom validation logic.
Configured in Designer
// Apply "Must" validation with custom expression
Generated Validator
RuleFor(x => x.EndDate)
    .Must((dto, endDate) => endDate > dto.StartDate)
    .WithMessage("'End Date' must be after 'Start Date'.");

Configuration

Module Settings

Application Settings > Fluent Validation (Application Layer)

Unique Constraint Validation

Options: Enabled by default | Disabled by default Default: Disabled by default Enables automatic validation for entity unique constraints.
When enabled, the module generates validation rules that check database uniqueness using repository queries.

Generate Stub Validators

Type: switch Default: true Generates empty validator classes for commands/queries even if no validation rules are defined.
Enable this to establish a validation structure that can be extended later with custom rules.

Cascade Mode

Control how validation proceeds when rules fail:
Cascade Mode Options
// Stop on first failure (default)
RuleFor(x => x.Email)
    .Cascade(CascadeMode.Stop)
    .NotEmpty()
    .EmailAddress();

// Continue validating all rules
RuleFor(x => x.Email)
    .Cascade(CascadeMode.Continue)
    .NotEmpty()
    .EmailAddress();
Apply the Fluent Validation stereotype with cascade mode settings in the designer.

Generated Validators

Command Validator Example

// CreateProductCommand
//   - Name (Required, Max Length: 100)
//   - Description (Min Length: 10)
//   - Price (Min Value: 0.01, Max Value: 999999.99)
//   - CategoryId (Required)

Nested Object Validation

Nested Validators
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator(IValidatorProvider validatorProvider)
    {
        ConfigureValidationRules(validatorProvider);
    }

    private void ConfigureValidationRules(IValidatorProvider validatorProvider)
    {
        RuleFor(v => v.OrderNumber)
            .NotEmpty();

        RuleFor(v => v.Customer)
            .NotNull()
            .SetValidator(validatorProvider.GetValidator<CreateCustomerDto>()!);

        RuleForEach(v => v.Items)
            .SetValidator(validatorProvider.GetValidator<CreateOrderItemDto>()!);
    }
}

Integration

MediatR Pipeline Validation

With the Intent.Application.MediatR.FluentValidation module:
Pipeline Behavior
public class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    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)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, cancellationToken)));

            var failures = validationResults
                .Where(r => !r.IsValid)
                .SelectMany(r => r.Errors)
                .ToList();

            if (failures.Any())
            {
                throw new ValidationException(failures);
            }
        }

        return await next();
    }
}
Validation runs automatically before command/query handlers execute.

ASP.NET Core Integration

With the Intent.AspNetCore.Controllers module:
Controller Action
[HttpPost]
public async Task<ActionResult<Guid>> CreateProduct(
    [FromBody] CreateProductCommand command,
    CancellationToken cancellationToken)
{
    // Validation happens automatically in MediatR pipeline
    var productId = await _mediator.Send(command, cancellationToken);
    return Created($"/api/products/{productId}", productId);
}
Validation errors are returned as:
Validation Error Response
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["'Name' must not exceed 100 characters."],
    "Price": ["'Price' must be between 0.01 and 999999.99."]
  }
}

Unique Constraint Validation

Validate database uniqueness constraints:
1

Apply Unique Index

Add a unique index to your entity in the Domain Designer
2

Enable Setting

Set “Unique Constraint Validation” to “Enabled by default”
3

Map DTO Field

Map your DTO field to the unique attribute
4

Generate

The validator will check database uniqueness

Generated Unique Validation

Unique Email Validator
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    private readonly IUserRepository _userRepository;

    public CreateUserCommandValidator(IUserRepository userRepository)
    {
        _userRepository = userRepository;
        ConfigureValidationRules();
    }

    private void ConfigureValidationRules()
    {
        RuleFor(v => v.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(CheckUniqueEmail)
            .WithMessage("Email '{PropertyValue}' already exists.");
    }

    private async Task<bool> CheckUniqueEmail(
        string email,
        CancellationToken cancellationToken)
    {
        return !await _userRepository.Query()
            .AnyAsync(x => x.Email == email, cancellationToken);
    }
}
Unique constraint validation requires database queries. Consider disabling for high-throughput scenarios and relying on database constraints instead.

Custom Validation Rules

Add custom validation logic in managed sections:
Custom Validation
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    [IntentManaged(Mode.Fully, Body = Mode.Ignore)]
    public CreateOrderCommandValidator(
        IProductRepository productRepository,
        ICustomerRepository customerRepository)
    {
        ConfigureValidationRules();
        
        // Custom validation
        RuleFor(v => v.CustomerId)
            .MustAsync(async (id, ct) => await CustomerExists(id, customerRepository, ct))
            .WithMessage("Customer does not exist.");

        RuleFor(v => v.Items)
            .MustAsync(async (items, ct) => await AllProductsExist(items, productRepository, ct))
            .WithMessage("One or more products do not exist.");
    }

    [IntentManaged(Mode.Fully)]
    private void ConfigureValidationRules()
    {
        // Designer-generated rules...
    }

    private static async Task<bool> CustomerExists(
        Guid customerId,
        ICustomerRepository repository,
        CancellationToken cancellationToken)
    {
        return await repository.FindByIdAsync(customerId, cancellationToken) != null;
    }

    private static async Task<bool> AllProductsExist(
        List<CreateOrderItemDto> items,
        IProductRepository repository,
        CancellationToken cancellationToken)
    {
        var productIds = items.Select(i => i.ProductId).ToList();
        var existingCount = await repository.Query()
            .Where(p => productIds.Contains(p.Id))
            .CountAsync(cancellationToken);
        return existingCount == productIds.Count;
    }
}

Best Practices

Validate input at application boundaries (commands, DTOs) rather than in domain entities. Keep domain logic separate from validation.
Provide clear, actionable error messages:
// ✅ Good
.WithMessage("Email address must be in a valid format (e.g., [email protected])")

// ❌ Bad
.WithMessage("Invalid")
Use CascadeMode.Stop for dependent validations to avoid confusing error messages:
RuleFor(x => x.Email)
    .Cascade(CascadeMode.Stop)
    .NotEmpty()           // Stop here if empty
    .EmailAddress()       // Only check format if not empty
    .MustAsync(IsUnique); // Only check uniqueness if format is valid
Each validator should handle a single command, query, or DTO. Don’t create god validators.

Troubleshooting

Issue: Validation rules aren’t being executed.Solution:
  1. Ensure validators are registered in DI (automatic with FluentValidation module)
  2. Check MediatR pipeline behavior is registered
  3. Verify the validator class name matches the command/query name + “Validator”
Issue: Slow validation due to database queries.Solution:
  1. Add database indexes on validated columns
  2. Consider disabling automatic unique validation
  3. Rely on database constraints and handle violation exceptions
Issue: Unhandled ValidationException.Solution: The Intent.AspNetCore modules automatically handle validation exceptions. Ensure you’re using compatible versions.

External Resources

Build docs developers (and LLMs) love