Skip to main content

Overview

The Intent.Application.FluentValidation.Dtos module generates FluentValidation validators specifically for DTOs. This ensures input validation at the application boundary before data reaches your services or domain logic.
Module: Intent.Application.FluentValidation.DtosVersion: 3.12.5+Dependencies:
  • Intent.Application.Dtos
  • Intent.Application.FluentValidation
  • Intent.Modelers.Services

Key Features

  • DTO-Specific Validators: Generate validators for DTOs used in service operations
  • Validation Service: Centralized validation infrastructure
  • Designer-Driven: Configure validation rules in the Services Designer
  • Automatic Registration: Validators registered in DI automatically
  • Integration: Works seamlessly with ASP.NET Core, Azure Functions, AWS Lambda, and FastEndpoints

Installation

intent install Intent.Application.FluentValidation.Dtos
This module extends Intent.Application.FluentValidation with DTO-specific capabilities. Both modules should be installed together.

How It Works

When you apply validation rules to DTO fields in the Services Designer, the module generates:
  1. DTO Validators: AbstractValidator<TDto> classes for each DTO
  2. Validation Service: Infrastructure for on-demand validation
  3. Validator Provider: Lookup mechanism for nested validators

Generated Components

DTO Validator

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

Validation Service

The module generates a validation service for programmatic validation:
IValidationService.cs
namespace MyApp.Application.Common.Validation
{
    public interface IValidationService
    {
        Task<ValidationResult> ValidateAsync<T>(
            T instance,
            CancellationToken cancellationToken = default);
    }
}
ValidationService.cs
using FluentValidation;

public class ValidationService : IValidationService
{
    private readonly IValidatorProvider _validatorProvider;

    public ValidationService(IValidatorProvider validatorProvider)
    {
        _validatorProvider = validatorProvider;
    }

    public async Task<ValidationResult> ValidateAsync<T>(
        T instance,
        CancellationToken cancellationToken = default)
    {
        var validator = _validatorProvider.GetValidator<T>();
        if (validator == null)
        {
            return new ValidationResult();
        }

        return await validator.ValidateAsync(instance, cancellationToken);
    }
}

Validator Provider

IValidatorProvider.cs
namespace MyApp.Application.Common.Validation
{
    public interface IValidatorProvider
    {
        IValidator<T>? GetValidator<T>();
    }
}
ValidatorProvider.cs
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;

public class ValidatorProvider : IValidatorProvider
{
    private readonly IServiceProvider _serviceProvider;

    public ValidatorProvider(IServiceProvider serviceProvider)
    {
    _serviceProvider = serviceProvider;
    }

    public IValidator<T>? GetValidator<T>()
    {
        return _serviceProvider.GetService<IValidator<T>>();
    }
}

Usage Examples

Programmatic Validation

Validate DTOs explicitly in your code:
Manual Validation
public class ProductService : IProductService
{
    private readonly IValidationService _validationService;
    private readonly IProductRepository _productRepository;

    public ProductService(
        IValidationService validationService,
        IProductRepository productRepository)
    {
        _validationService = validationService;
        _productRepository = productRepository;
    }

    public async Task<Guid> ImportProductAsync(
        ProductDto productDto,
        CancellationToken cancellationToken)
    {
        // Explicit validation
        var validationResult = await _validationService
            .ValidateAsync(productDto, cancellationToken);

        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        // Process valid product...
        var product = new Product
        {
            Name = productDto.Name,
            Price = productDto.Price
        };

        await _productRepository.AddAsync(product, cancellationToken);
        return product.Id;
    }
}

Nested DTO Validation

Validators automatically handle nested DTOs:
Nested DTOs
public class OrderDto
{
    public string OrderNumber { get; set; }
    public CustomerDto Customer { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class OrderDtoValidator : AbstractValidator<OrderDto>
{
    public OrderDtoValidator(IValidatorProvider validatorProvider)
    {
        ConfigureValidationRules(validatorProvider);
    }

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

        // Validate nested Customer DTO
        RuleFor(v => v.Customer)
            .NotNull()
            .SetValidator(validatorProvider.GetValidator<CustomerDto>()!);

        // Validate each item in collection
        RuleForEach(v => v.Items)
            .SetValidator(validatorProvider.GetValidator<OrderItemDto>()!);
    }
}

ASP.NET Core Integration

With ASP.NET Core controllers, validation happens automatically:
Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpPost]
    public async Task<ActionResult<Guid>> CreateProduct(
        [FromBody] CreateProductDto dto,
        CancellationToken cancellationToken)
    {
        // Validation happens automatically via MediatR pipeline
        // or service-level validation
        var productId = await _productService
            .CreateProductAsync(dto, cancellationToken);
        
        return Created($"/api/products/{productId}", productId);
    }
}
Invalid requests return:
Validation Error
{
  "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 be empty."],
    "Price": ["'Price' must be greater than or equal to 0.01."]
  }
}

Validation Rules

DTO validators support all validation rules from the base FluentValidation module:

String Rules

  • Required
  • Min Length
  • Max Length
  • Email Address
  • Regular Expression

Numeric Rules

  • Min Value
  • Max Value
  • Range
  • Precision/Scale

Collection Rules

  • Not Empty
  • Min/Max Count
  • ForEach validation

Special Rules

  • Enum validation
  • Custom Must rules
  • Nested object validation
See the FluentValidation module documentation for detailed rule descriptions.

Service Integration Patterns

Pattern 1: Service-Level Validation

Validate DTOs at the service layer:
Service with Validation
public class OrderService : IOrderService
{
    private readonly IValidationService _validationService;
    private readonly IOrderRepository _orderRepository;

    public async Task<Guid> CreateOrderAsync(
        OrderDto orderDto,
        CancellationToken cancellationToken)
    {
        await _validationService.ValidateAsync(orderDto, cancellationToken)
            ?? throw new ValidationException("Validation failed");

        // Create order...
    }
}

Pattern 2: MediatR Pipeline Validation

With the Intent.Application.MediatR.FluentValidation module:
Command with DTO
public class CreateOrderCommand : IRequest<Guid>
{
    public OrderDto Order { get; set; }
}

public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator(IValidatorProvider validatorProvider)
    {
        RuleFor(v => v.Order)
            .NotNull()
            .SetValidator(validatorProvider.GetValidator<OrderDto>()!);
    }
}
Validation runs automatically in the MediatR pipeline before the handler executes.

Pattern 3: Batch Validation

Validate multiple DTOs:
Batch Import
public async Task<List<ValidationResult>> ValidateBatchAsync(
    List<ProductDto> products,
    CancellationToken cancellationToken)
{
    var results = new List<ValidationResult>();

    foreach (var product in products)
    {
        var result = await _validationService
            .ValidateAsync(product, cancellationToken);
        results.Add(result);
    }

    return results;
}

Custom Validation Logic

Extend generated validators with custom rules:
Custom DTO Validation
[IntentManaged(Mode.Merge, Signature = Mode.Fully)]
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
    [IntentManaged(Mode.Fully, Body = Mode.Ignore)]
    public CreateProductDtoValidator(
        ICategoryRepository categoryRepository,
        IProductRepository productRepository)
    {
        ConfigureValidationRules();

        // Custom: Verify category exists
        RuleFor(v => v.CategoryId)
            .MustAsync(async (id, ct) => 
            {
                return await categoryRepository.FindByIdAsync(id, ct) != null;
            })
            .WithMessage("Category does not exist.");

        // Custom: Check for duplicate SKU
        RuleFor(v => v.Sku)
            .MustAsync(async (sku, ct) =>
            {
                return !await productRepository.Query()
                    .AnyAsync(p => p.Sku == sku, ct);
            })
            .WithMessage("SKU '{PropertyValue}' already exists.");

        // Custom: Business rule validation
        RuleFor(v => v)
            .Must(dto => dto.DiscountPrice == null || dto.DiscountPrice < dto.Price)
            .WithMessage("Discount price must be less than regular price.")
            .When(v => v.DiscountPrice.HasValue);
    }

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

Testing Validators

Unit test your DTO validators:
Validator Tests
public class ProductDtoValidatorTests
{
    private readonly ProductDtoValidator _validator;

    public ProductDtoValidatorTests()
    {
        _validator = new ProductDtoValidator();
    }

    [Fact]
    public async Task Validate_WithValidProduct_ShouldPass()
    {
        // Arrange
        var dto = new ProductDto
        {
            Name = "Test Product",
            Description = "This is a test product description",
            Price = 29.99m,
            CategoryId = Guid.NewGuid()
        };

        // Act
        var result = await _validator.ValidateAsync(dto);

        // Assert
        Assert.True(result.IsValid);
    }

    [Fact]
    public async Task Validate_WithEmptyName_ShouldFail()
    {
        // Arrange
        var dto = new ProductDto
        {
            Name = "",
            Price = 29.99m
        };

        // Act
        var result = await _validator.ValidateAsync(dto);

        // Assert
        Assert.False(result.IsValid);
        Assert.Contains(result.Errors, e => e.PropertyName == "Name");
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(0)]
    public async Task Validate_WithInvalidPrice_ShouldFail(decimal price)
    {
        // Arrange
        var dto = new ProductDto
        {
            Name = "Test Product",
            Price = price
        };

        // Act
        var result = await _validator.ValidateAsync(dto);

        // Assert
        Assert.False(result.IsValid);
        Assert.Contains(result.Errors, e => e.PropertyName == "Price");
    }
}

Best Practices

Validate DTOs as soon as they enter your application (controllers, message handlers, etc.) before they reach business logic.
DTO validators should focus on format, structure, and basic constraints. Complex business rules belong in domain entities or services.
// ✅ Good - Format validation
RuleFor(v => v.Email).EmailAddress();

// ❌ Bad - Business logic in validator
RuleFor(v => v.OrderTotal)
    .Must(total => CalculateComplexDiscountLogic(total) > 0)
    .WithMessage("Order doesn't qualify for discount");
For complex DTOs, create validators for nested objects rather than inline validation:
// ✅ Good
RuleFor(v => v.Address)
    .SetValidator(validatorProvider.GetValidator<AddressDto>()!);

// ❌ Bad
RuleFor(v => v.Address.Street).NotEmpty();
RuleFor(v => v.Address.City).NotEmpty();
RuleFor(v => v.Address.ZipCode).Matches("[0-9]{5}");
Customize error messages to be clear and actionable:
RuleFor(v => v.Email)
    .EmailAddress()
    .WithMessage("Please provide a valid email address (e.g., [email protected])");

Performance Considerations

Async Validation: Use MustAsync for database lookups, but be aware of the performance impact. Consider:
  • Caching frequently validated data
  • Batch validation for imports
  • Deferring some checks to domain logic
Optimized Async Validation
private readonly IMemoryCache _cache;

RuleFor(v => v.CategoryId)
    .MustAsync(async (id, ct) =>
    {
        // Check cache first
        if (_cache.TryGetValue($"category_{id}", out _))
            return true;

        var exists = await _categoryRepository
            .FindByIdAsync(id, ct) != null;

        if (exists)
            _cache.Set($"category_{id}", true, TimeSpan.FromMinutes(5));

        return exists;
    });

Troubleshooting

Issue: IValidatorProvider.GetValidator<T>() returns null.Solution:
  1. Verify validator is generated for the DTO
  2. Check DI registration (should be automatic)
  3. Ensure you’re using the correct DTO type
  4. Rebuild the solution
Issue: Validators are registered but not executing.Solution:
  1. Ensure IValidationService is being called
  2. Check MediatR pipeline configuration (if using commands/queries)
  3. Verify ASP.NET Core validation filters are registered
Issue: Nested DTO validation fails with null reference exceptions.Solution:
  1. Add null checks before nested validation:
    RuleFor(v => v.Address)
        .NotNull()
        .SetValidator(...);
    
  2. Ensure nested validators are registered

External Resources

Build docs developers (and LLMs) love