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:
DTO Validators : AbstractValidator<TDto> classes for each DTO
Validation Service : Infrastructure for on-demand validation
Validator Provider : Lookup mechanism for nested validators
Generated Components
DTO Validator
Designer Configuration
Generated 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:
namespace MyApp . Application . Common . Validation
{
public interface IValidationService
{
Task < ValidationResult > ValidateAsync < T >(
T instance ,
CancellationToken cancellationToken = default );
}
}
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
namespace MyApp . Application . Common . Validation
{
public interface IValidatorProvider
{
IValidator < T >? GetValidator < T >();
}
}
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:
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:
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:
[ 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:
{
"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:
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...
}
}
With the Intent.Application.MediatR.FluentValidation module:
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:
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:
[ 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:
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}" );
Provide Meaningful Messages
Customize error messages to be clear and actionable: RuleFor ( v => v . Email )
. EmailAddress ()
. WithMessage ( "Please provide a valid email address (e.g., [email protected] )" );
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 :
Verify validator is generated for the DTO
Check DI registration (should be automatic)
Ensure you’re using the correct DTO type
Rebuild the solution
Issue : Validators are registered but not executing.Solution :
Ensure IValidationService is being called
Check MediatR pipeline configuration (if using commands/queries)
Verify ASP.NET Core validation filters are registered
Issue : Nested DTO validation fails with null reference exceptions.Solution :
Add null checks before nested validation:
RuleFor ( v => v . Address )
. NotNull ()
. SetValidator ( .. .);
Ensure nested validators are registered
External Resources