# Adding Validation Add input validation to your commands and queries using FluentValidation. ## Why Validation? Validation ensures: - ✅ Data integrity - Only valid data enters your system - ✅ Security - Prevent injection attacks and malformed input - ✅ User experience - Clear, structured error messages - ✅ Business rules - Enforce domain constraints ## Install FluentValidation Add the required packages: ```bash dotnet add package Svrnty.CQRS.FluentValidation dotnet add package FluentValidation ``` ## Step 1: Create a Validator Let's add validation to the `CreateUserCommand` from the previous guide. Create `Validators/CreateUserCommandValidator.cs`: ```csharp using FluentValidation; using MyApp.Commands; namespace MyApp.Validators; 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() .WithMessage("Email is required") .EmailAddress() .WithMessage("Email must be a valid email address"); } } ``` ## Step 2: Register the Validator ### Option 1: Register Command with Validator ```csharp // Program.cs builder.Services.AddCommand(); ``` This single line registers: 1. The command handler 2. The validator 3. The metadata for discovery ### Option 2: Register Separately ```csharp // Register command builder.Services.AddCommand(); // Register validator builder.Services.AddTransient, CreateUserCommandValidator>(); ``` ## Step 3: Test Validation ### Valid Request ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{ "name": "Alice Smith", "email": "alice@example.com" }' ``` **Response (200 OK):** ```json 123 ``` ### Invalid Request ```bash curl -X POST http://localhost:5000/api/command/createUser \ -H "Content-Type: application/json" \ -d '{ "name": "", "email": "invalid-email" }' ``` **HTTP Response (400 Bad Request):** ```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": [ "Email must be a valid email address" ] } } ``` This follows **RFC 7807** (Problem Details for HTTP APIs). ## Validation Rules FluentValidation provides many built-in validators: ### Required Fields ```csharp RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required"); ``` ### String Length ```csharp RuleFor(x => x.Name) .MinimumLength(3) .WithMessage("Name must be at least 3 characters") .MaximumLength(100) .WithMessage("Name must not exceed 100 characters"); ``` ### Email Validation ```csharp RuleFor(x => x.Email) .EmailAddress() .WithMessage("Email must be a valid email address"); ``` ### Numeric Range ```csharp RuleFor(x => x.Age) .GreaterThan(0) .WithMessage("Age must be greater than 0") .LessThanOrEqualTo(120) .WithMessage("Age must be less than or equal to 120"); ``` ### Regular Expression ```csharp RuleFor(x => x.PhoneNumber) .Matches(@"^\d{3}-\d{3}-\d{4}$") .WithMessage("Phone number must be in format: 123-456-7890"); ``` ### Must (Custom Rule) ```csharp RuleFor(x => x.StartDate) .Must(BeAFutureDate) .WithMessage("Start date must be in the future"); private bool BeAFutureDate(DateTime date) { return date > DateTime.UtcNow; } ``` ### Nested Object Validation ```csharp public record CreateOrderCommand { public AddressDto ShippingAddress { get; init; } = null!; public List Items { get; init; } = new(); } public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { RuleFor(x => x.ShippingAddress) .NotNull() .SetValidator(new AddressValidator()); RuleForEach(x => x.Items) .SetValidator(new OrderItemValidator()); RuleFor(x => x.Items) .NotEmpty() .WithMessage("Order must contain at least one item"); } } public class AddressValidator : AbstractValidator { public AddressValidator() { RuleFor(x => x.Street).NotEmpty(); RuleFor(x => x.City).NotEmpty(); RuleFor(x => x.ZipCode).Matches(@"^\d{5}$"); } } ``` ## Complete Validation Example Here's a comprehensive validator: ```csharp using FluentValidation; namespace MyApp.Validators; public class CreateUserCommandValidator : AbstractValidator { private readonly IUserRepository _userRepository; public CreateUserCommandValidator(IUserRepository userRepository) { _userRepository = userRepository; // Required fields RuleFor(x => x.Name) .NotEmpty().WithMessage("Name is required") .MaximumLength(100).WithMessage("Name must not exceed 100 characters"); RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Email must be a valid email address") .MustAsync(BeUniqueEmail).WithMessage("Email already exists"); RuleFor(x => x.Age) .GreaterThan(0).WithMessage("Age must be greater than 0") .LessThanOrEqualTo(120).WithMessage("Age must be realistic"); RuleFor(x => x.PhoneNumber) .Matches(@"^\d{3}-\d{3}-\d{4}$") .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) .WithMessage("Phone number must be in format: 123-456-7890"); } private async Task BeUniqueEmail(string email, CancellationToken cancellationToken) { var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken); return existingUser == null; } } ``` ## Async Validation For validation that requires database access or external API calls: ```csharp RuleFor(x => x.Email) .MustAsync(BeUniqueEmail) .WithMessage("Email already exists"); private async Task BeUniqueEmail(string email, CancellationToken cancellationToken) { var exists = await _userRepository.EmailExistsAsync(email, cancellationToken); return !exists; } ``` ## Conditional Validation Validate only when certain conditions are met: ```csharp // Validate only when property is not null RuleFor(x => x.ShippingAddress) .SetValidator(new AddressValidator()) .When(x => x.ShippingAddress != null); // Validate based on other property RuleFor(x => x.CreditCardNumber) .NotEmpty() .When(x => x.PaymentMethod == "CreditCard") .WithMessage("Credit card number is required for credit card payments"); ``` ## Validating Queries Queries can also be validated: ```csharp // Query public record SearchUsersQuery { public string Keyword { get; init; } = string.Empty; public int Page { get; init; } = 1; public int PageSize { get; init; } = 10; } // Validator public class SearchUsersQueryValidator : AbstractValidator { public SearchUsersQueryValidator() { RuleFor(x => x.Keyword) .MinimumLength(3) .When(x => !string.IsNullOrEmpty(x.Keyword)) .WithMessage("Keyword must be at least 3 characters"); RuleFor(x => x.Page) .GreaterThan(0) .WithMessage("Page must be greater than 0"); RuleFor(x => x.PageSize) .InclusiveBetween(1, 100) .WithMessage("Page size must be between 1 and 100"); } } // Registration builder.Services.AddQuery, SearchUsersQueryHandler, SearchUsersQueryValidator>(); ``` ## HTTP vs gRPC Validation ### HTTP (Minimal API) Validation errors return **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": { "Email": ["Email is required", "Email must be a valid email address"], "Age": ["Age must be greater than 0"] } } ``` **HTTP Status:** `400 Bad Request` ### gRPC Validation errors return **Google Rich Error Model**: ```protobuf status { code: 3 // INVALID_ARGUMENT message: "Validation failed" details: [ google.rpc.BadRequest { field_violations: [ { field: "Email", description: "Email is required" }, { field: "Email", description: "Email must be a valid email address" }, { field: "Age", description: "Age must be greater than 0" } ] } ] } ``` **gRPC Status Code:** `INVALID_ARGUMENT` Both formats are automatically generated by Svrnty.CQRS! ## Validation Best Practices ### ✅ DO - **Validate early** - At the API boundary - **Use descriptive messages** - Help users fix errors - **Validate business rules** - Not just data types - **Use async validation** - For database checks - **Return all errors** - Don't stop at first error - **Validate commands AND queries** - Both need validation ### ❌ DON'T - **Don't validate in handlers** - Use validators - **Don't use exceptions** - Let FluentValidation handle it - **Don't skip validation** - Even for "internal" commands - **Don't return generic messages** - Be specific - **Don't over-validate** - Balance security and usability ## Custom Validators Create reusable validators: ```csharp public static class CustomValidators { public static IRuleBuilderOptions MustBeValidUrl( this IRuleBuilder ruleBuilder) { return ruleBuilder .Must(url => Uri.TryCreate(url, UriKind.Absolute, out _)) .WithMessage("'{PropertyName}' must be a valid URL"); } public static IRuleBuilderOptions MustBeStrongPassword( this IRuleBuilder ruleBuilder) { return ruleBuilder .MinimumLength(8).WithMessage("Password must be at least 8 characters") .Matches(@"[A-Z]").WithMessage("Password must contain uppercase letter") .Matches(@"[a-z]").WithMessage("Password must contain lowercase letter") .Matches(@"\d").WithMessage("Password must contain digit") .Matches(@"[^\w]").WithMessage("Password must contain special character"); } } // Usage RuleFor(x => x.Website) .MustBeValidUrl(); RuleFor(x => x.Password) .MustBeStrongPassword(); ``` ## Troubleshooting ### Validation Not Running **Problem:** Requests succeed even with invalid data **Solutions:** 1. Ensure you installed `Svrnty.CQRS.FluentValidation` 2. Verify validator is registered in DI 3. Check validator class inherits `AbstractValidator` ### Validation Always Fails **Problem:** All requests return 400 even with valid data **Solutions:** 1. Check validator rules are correct 2. Verify async validators return correct boolean 3. Ensure property names match exactly ### Multiple Validators Registered **Problem:** Conflicting validation rules **Solutions:** 1. Only register one validator per command/query 2. Combine rules in a single validator 3. Use `RuleSet` for conditional validation ## What's Next? Now you know how to add validation! Let's discuss when to use HTTP vs gRPC. **Continue to [Choosing HTTP vs gRPC](06-choosing-http-or-grpc.md) →** ## See Also - [HTTP Validation](../core-features/validation/http-validation.md) - RFC 7807 Problem Details - [gRPC Validation](../core-features/validation/grpc-validation.md) - Google Rich Error Model - [Custom Validation](../core-features/validation/custom-validation.md) - Advanced validation scenarios - [FluentValidation Documentation](https://docs.fluentvalidation.net/) - Official FluentValidation docs