# 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/)