dotnet-cqrs/docs/core-features/validation/README.md

344 lines
8.7 KiB
Markdown

# 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<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
```csharp
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)
```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<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:
```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)