344 lines
8.7 KiB
Markdown
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)
|