8.7 KiB
8.7 KiB
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
using FluentValidation;
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
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
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddTransient<IValidator<CreateUserCommand>, 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)
{
"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)
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
Setting up validators:
- Installing FluentValidation
- Creating validators
- Registering validators
- Common validation rules
HTTP Validation
HTTP-specific validation:
- RFC 7807 Problem Details
- ASP.NET Core integration
- Model state errors
- Custom error responses
gRPC Validation
gRPC-specific validation:
- Google Rich Error Model
- Field violations
- Error details
- Client error handling
Custom Validation
Advanced validation scenarios:
- Custom validators
- Async validation
- Database validation
- Cross-property validation
- Conditional validation
Common Validation Rules
Required Fields
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required");
String Length
RuleFor(x => x.Description)
.MaximumLength(500)
.WithMessage("Description must not exceed 500 characters");
RuleFor(x => x.Username)
.MinimumLength(3)
.MaximumLength(20);
Email Validation
RuleFor(x => x.Email)
.EmailAddress()
.WithMessage("Valid email address is required");
Numeric Ranges
RuleFor(x => x.Age)
.GreaterThanOrEqualTo(0)
.LessThanOrEqualTo(150);
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 100);
Regular Expressions
RuleFor(x => x.PhoneNumber)
.Matches(@"^\+?[1-9]\d{1,14}$")
.WithMessage("Invalid phone number format");
Custom Predicates
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:
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
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<bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
{
var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken);
return existingUser == null;
}
}
Validation Severity
FluentValidation supports different severity levels:
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
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 - Install and configure validators
- HTTP Validation - RFC 7807 Problem Details
- gRPC Validation - Google Rich Error Model
- Custom Validation - Advanced validation scenarios