# FluentValidation Setup How to install, configure, and use FluentValidation with Svrnty.CQRS. ## Installation ### Install NuGet Package ```bash dotnet add package FluentValidation dotnet add package FluentValidation.DependencyInjectionExtensions ``` ### Package Reference ```xml ``` ## Creating Validators Validators inherit from `AbstractValidator` and define rules in the constructor. ### Basic Validator ```csharp using FluentValidation; public class CreateUserCommand { public string Name { get; init; } = string.Empty; public string Email { get; init; } = string.Empty; public int Age { get; init; } } public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required") .MaximumLength(100) .WithMessage("Name must not exceed 100 characters"); RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .WithMessage("Valid email address is required"); RuleFor(x => x.Age) .GreaterThanOrEqualTo(18) .WithMessage("User must be at least 18 years old") .LessThanOrEqualTo(150) .WithMessage("Age must be realistic"); } } ``` ### Validator with Dependencies ```csharp public class CreateProductCommandValidator : AbstractValidator { private readonly IProductRepository _productRepository; public CreateProductCommandValidator(IProductRepository productRepository) { _productRepository = productRepository; RuleFor(x => x.Name) .NotEmpty() .MustAsync(BeUniqueName) .WithMessage("Product name already exists"); RuleFor(x => x.Price) .GreaterThan(0) .WithMessage("Price must be greater than zero"); RuleFor(x => x.CategoryId) .NotEmpty() .WithMessage("Category is required"); } private async Task BeUniqueName(string name, CancellationToken cancellationToken) { var existing = await _productRepository.GetByNameAsync(name, cancellationToken); return existing == null; } } ``` ## Registering Validators ### Individual Registration ```csharp builder.Services.AddTransient, CreateUserCommandValidator>(); ``` ### Automatic Registration Register all validators in an assembly: ```csharp using FluentValidation; builder.Services.AddValidatorsFromAssemblyContaining(); ``` ### Registration with Command ```csharp // Register command builder.Services.AddCommand(); // Register validator builder.Services.AddTransient, CreateUserCommandValidator>(); ``` ### Bulk Registration Extension ```csharp public static class ValidationRegistration { public static IServiceCollection AddCommandValidators(this IServiceCollection services) { // Register all validators services.AddValidatorsFromAssemblyContaining(); return services; } } // Usage builder.Services.AddCommandValidators(); ``` ## Common Validation Rules ### Required Fields ```csharp // Not null or empty RuleFor(x => x.Name) .NotEmpty(); // Not null RuleFor(x => x.UserId) .NotNull(); ``` ### String Validation ```csharp // Length RuleFor(x => x.Username) .MinimumLength(3) .MaximumLength(20); // Exact length RuleFor(x => x.PostalCode) .Length(5); // Email RuleFor(x => x.Email) .EmailAddress(); // Regular expression RuleFor(x => x.PhoneNumber) .Matches(@"^\+?[1-9]\d{1,14}$") .WithMessage("Invalid phone number format"); ``` ### Numeric Validation ```csharp // Greater than RuleFor(x => x.Age) .GreaterThan(0); // Greater than or equal RuleFor(x => x.Quantity) .GreaterThanOrEqualTo(1); // Less than RuleFor(x => x.Discount) .LessThan(100); // Range RuleFor(x => x.Rating) .InclusiveBetween(1, 5); ``` ### Collection Validation ```csharp // Not empty collection RuleFor(x => x.Items) .NotEmpty() .WithMessage("At least one item is required"); // Collection count RuleFor(x => x.Tags) .Must(tags => tags.Count <= 10) .WithMessage("Maximum 10 tags allowed"); // Validate each item RuleForEach(x => x.Items) .SetValidator(new OrderItemValidator()); ``` ### Complex Object Validation ```csharp // Nested object RuleFor(x => x.Address) .NotNull() .SetValidator(new AddressValidator()); // Conditional validation RuleFor(x => x.CompanyName) .NotEmpty() .When(x => x.IsCompany); ``` ### Custom Predicates ```csharp // Must satisfy predicate RuleFor(x => x.StartDate) .Must(date => date > DateTime.UtcNow) .WithMessage("Start date must be in the future"); // Must satisfy async predicate RuleFor(x => x.Email) .MustAsync(async (email, cancellationToken) => { var exists = await _userRepository.EmailExistsAsync(email, cancellationToken); return !exists; }) .WithMessage("Email is already registered"); ``` ## Conditional Validation ### When ```csharp RuleFor(x => x.CompanyName) .NotEmpty() .When(x => x.IsCompany); RuleFor(x => x.TaxId) .NotEmpty() .When(x => x.IsCompany); ``` ### Unless ```csharp RuleFor(x => x.MiddleName) .NotEmpty() .Unless(x => x.PreferNoMiddleName); ``` ## Cross-Property Validation ```csharp public class DateRangeValidator : AbstractValidator { public DateRangeValidator() { RuleFor(x => x.EndDate) .GreaterThanOrEqualTo(x => x.StartDate) .WithMessage("End date must be after start date"); RuleFor(x => x) .Must(q => (q.EndDate - q.StartDate).TotalDays <= 90) .WithMessage("Date range must not exceed 90 days"); } } ``` ## Async Validation ### Database Lookup ```csharp public class CreateUserCommandValidator : AbstractValidator { private readonly IUserRepository _userRepository; public CreateUserCommandValidator(IUserRepository userRepository) { _userRepository = userRepository; RuleFor(x => x.Email) .MustAsync(BeUniqueEmail) .WithMessage("Email is already registered"); } private async Task BeUniqueEmail(string email, CancellationToken cancellationToken) { var user = await _userRepository.GetByEmailAsync(email, cancellationToken); return user == null; } } ``` ### External API Call ```csharp public class ValidateAddressCommandValidator : AbstractValidator { private readonly IAddressValidationService _validationService; public ValidateAddressCommandValidator(IAddressValidationService validationService) { _validationService = validationService; RuleFor(x => x.ZipCode) .MustAsync(BeValidZipCode) .WithMessage("Invalid zip code"); } private async Task BeValidZipCode(string zipCode, CancellationToken cancellationToken) { return await _validationService.IsValidZipCodeAsync(zipCode, cancellationToken); } } ``` ## Validation Messages ### Custom Messages ```csharp RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required"); RuleFor(x => x.Age) .GreaterThanOrEqualTo(18) .WithMessage("You must be at least 18 years old to register"); ``` ### Placeholder Messages ```csharp RuleFor(x => x.Name) .MaximumLength(100) .WithMessage("Name must not exceed {MaxLength} characters. You entered {TotalLength} characters."); RuleFor(x => x.Quantity) .InclusiveBetween(1, 100) .WithMessage("Quantity must be between {From} and {To}. You entered {PropertyValue}."); ``` ### Property Name Overrides ```csharp RuleFor(x => x.EmailAddress) .NotEmpty() .WithName("Email"); // Error: "Email is required" (instead of "Email Address is required") ``` ## Rule Sets ```csharp public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { // Default rules (always run) RuleFor(x => x.Name) .NotEmpty(); // Rule set for creating RuleSet("Create", () => { RuleFor(x => x.Email) .MustAsync(BeUniqueEmail); }); // Rule set for updating RuleSet("Update", () => { RuleFor(x => x.UserId) .GreaterThan(0); }); } } // Validate with specific rule set var result = await validator.ValidateAsync(command, options => { options.IncludeRuleSets("Create"); }); ``` ## Testing Validators ```csharp using FluentValidation.TestHelper; using Xunit; public class CreateUserCommandValidatorTests { private readonly CreateUserCommandValidator _validator; public CreateUserCommandValidatorTests() { _validator = new CreateUserCommandValidator(); } [Fact] public void Should_Require_Name() { var command = new CreateUserCommand { Name = "" }; var result = _validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Name); } [Fact] public void Should_Require_Valid_Email() { var command = new CreateUserCommand { Email = "invalid" }; var result = _validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Email); } [Fact] public void Should_Reject_Under_Age() { var command = new CreateUserCommand { Age = 16 }; var result = _validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Age); } [Fact] public void Should_Accept_Valid_Command() { var command = new CreateUserCommand { Name = "John Doe", Email = "john@example.com", Age = 25 }; var result = _validator.TestValidate(command); result.ShouldNotHaveAnyValidationErrors(); } } ``` ## Best Practices ### ✅ DO - Create one validator per command/query - Use descriptive error messages - Keep validators focused and single-purpose - Use async validation for I/O operations - Test validators independently - Register validators with DI - Use rule sets for complex scenarios ### ❌ DON'T - Don't perform business logic in validators - Don't modify state in validators - Don't throw exceptions (return validation results) - Don't validate domain entities (validate DTOs/commands) - Don't skip async validation when needed - Don't create validators without tests ## See Also - [Validation Overview](README.md) - [HTTP Validation](http-validation.md) - [gRPC Validation](grpc-validation.md) - [Custom Validation](custom-validation.md) - [FluentValidation Documentation](https://docs.fluentvalidation.net/)