# Validation Overview Input validation ensures data integrity and provides clear error messages to clients. ## What is Validation? Validation is the process of verifying that commands and queries contain valid data before processing. The framework integrates with **FluentValidation** to provide: - ✅ **Declarative validation rules** - Define rules with fluent syntax - ✅ **Automatic validation** - Execute before handler invocation - ✅ **Structured error responses** - RFC 7807 (HTTP) or Google Rich Error Model (gRPC) - ✅ **Async validation** - Database lookups, external API calls - ✅ **Reusable validators** - Share validation logic across commands/queries - ✅ **Custom validation** - Extend with custom rules ## Validation Flow ``` ┌─────────────┐ │ Request │ └──────┬──────┘ │ ▼ ┌──────────────────┐ │ Model Binding │ └──────┬───────────┘ │ ▼ ┌──────────────────┐ Validation ┌────────────────┐ │ Validator │─────fails────────▶│ Error Response│ └──────┬───────────┘ └────────────────┘ │ │ Validation passes ▼ ┌──────────────────┐ │ Authorization │ └──────┬───────────┘ │ ▼ ┌──────────────────┐ │ Handler │ └──────────────────┘ ``` ## Quick Example ### Define Validator ```csharp using FluentValidation; 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"); } } ``` ### Register Validator ```csharp builder.Services.AddCommand(); builder.Services.AddTransient, CreateUserCommandValidator>(); ``` ### Automatic Validation Validation happens automatically before the handler executes. If validation fails, the framework returns structured error responses without invoking the handler. ## HTTP vs gRPC Validation ### HTTP (RFC 7807 Problem Details) ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["Name is required"], "Email": ["Valid email address is required"], "Age": ["User must be at least 18 years old"] } } ``` ### gRPC (Google Rich Error Model) ```protobuf google.rpc.Status { code: 3 // INVALID_ARGUMENT message: "Validation failed" details: [ google.rpc.BadRequest { field_violations: [ { field: "name", description: "Name is required" }, { field: "email", description: "Valid email address is required" }, { field: "age", description: "User must be at least 18 years old" } ] } ] } ``` ## Validation Documentation ### [FluentValidation Setup](fluentvalidation-setup.md) Setting up validators: - Installing FluentValidation - Creating validators - Registering validators - Common validation rules ### [HTTP Validation](http-validation.md) HTTP-specific validation: - RFC 7807 Problem Details - ASP.NET Core integration - Model state errors - Custom error responses ### [gRPC Validation](grpc-validation.md) gRPC-specific validation: - Google Rich Error Model - Field violations - Error details - Client error handling ### [Custom Validation](custom-validation.md) Advanced validation scenarios: - Custom validators - Async validation - Database validation - Cross-property validation - Conditional validation ## Common Validation Rules ### Required Fields ```csharp RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required"); ``` ### String Length ```csharp RuleFor(x => x.Description) .MaximumLength(500) .WithMessage("Description must not exceed 500 characters"); RuleFor(x => x.Username) .MinimumLength(3) .MaximumLength(20); ``` ### Email Validation ```csharp RuleFor(x => x.Email) .EmailAddress() .WithMessage("Valid email address is required"); ``` ### Numeric Ranges ```csharp RuleFor(x => x.Age) .GreaterThanOrEqualTo(0) .LessThanOrEqualTo(150); RuleFor(x => x.Quantity) .InclusiveBetween(1, 100); ``` ### Regular Expressions ```csharp RuleFor(x => x.PhoneNumber) .Matches(@"^\+?[1-9]\d{1,14}$") .WithMessage("Invalid phone number format"); ``` ### Custom Predicates ```csharp RuleFor(x => x.StartDate) .Must(date => date > DateTime.UtcNow) .WithMessage("Start date must be in the future"); ``` ## Async Validation Validation can be asynchronous for database lookups or external API calls: ```csharp public class CreateUserCommandValidator : AbstractValidator { private readonly IUserRepository _userRepository; public CreateUserCommandValidator(IUserRepository userRepository) { _userRepository = userRepository; RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MustAsync(BeUniqueEmail) .WithMessage("Email address is already in use"); } private async Task BeUniqueEmail(string email, CancellationToken cancellationToken) { var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken); return existingUser == null; } } ``` ## Validation Severity FluentValidation supports different severity levels: ```csharp RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required") .WithSeverity(Severity.Error); RuleFor(x => x.Description) .MaximumLength(500) .WithMessage("Description is longer than recommended") .WithSeverity(Severity.Warning); RuleFor(x => x.Tags) .Must(tags => tags.Count <= 10) .WithMessage("Consider using fewer tags for better organization") .WithSeverity(Severity.Info); ``` ## Best Practices ### ✅ DO - Validate all user input - Use descriptive error messages - Validate at the boundary (commands/queries) - Use async validation for database checks - Keep validators focused and single-purpose - Reuse validators across similar commands - Test validators independently ### ❌ DON'T - Don't validate in handlers (validate earlier) - Don't throw exceptions for validation errors - Don't skip validation for internal commands - Don't perform business logic in validators - Don't validate domain entities (validate DTOs/commands) - Don't return generic error messages ## Testing Validators ```csharp using FluentValidation.TestHelper; [Fact] public void Should_Require_Name() { var validator = new CreateUserCommandValidator(); var command = new CreateUserCommand { Name = "" }; var result = validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Name) .WithErrorMessage("Name is required"); } [Fact] public void Should_Reject_Invalid_Email() { var validator = new CreateUserCommandValidator(); var command = new CreateUserCommand { Email = "invalid-email" }; var result = validator.TestValidate(command); result.ShouldHaveValidationErrorFor(x => x.Email); } [Fact] public void Should_Pass_Valid_Command() { var validator = new CreateUserCommandValidator(); var command = new CreateUserCommand { Name = "John Doe", Email = "john@example.com", Age = 25 }; var result = validator.TestValidate(command); result.ShouldNotHaveAnyValidationErrors(); } ``` ## What's Next? - **[FluentValidation Setup](fluentvalidation-setup.md)** - Install and configure validators - **[HTTP Validation](http-validation.md)** - RFC 7807 Problem Details - **[gRPC Validation](grpc-validation.md)** - Google Rich Error Model - **[Custom Validation](custom-validation.md)** - Advanced validation scenarios ## See Also - [Commands Overview](../commands/README.md) - [Queries Overview](../queries/README.md) - [Getting Started: Adding Validation](../../getting-started/05-adding-validation.md) - [Best Practices: Security](../../best-practices/security.md)