12. Model Validation
- Model Validation
About this chapter
Validate incoming data using both data annotations and FluentValidation to ensure data integrity and provide clear error messages to clients.
- Data annotation validation: Required, MaxLength, MinLength, Range attributes
- FluentValidation introduction: More powerful, testable validation framework
- FluentValidation setup: Installing packages and configuring in Program.cs
- Custom validators: Building reusable validation rules
- Async validation: Validating against database constraints
- Error response handling: Returning validation errors to clients
Learning outcomes:
- Apply data annotations for basic validation
- Install and configure FluentValidation in .NET 10
- Create custom FluentValidation rules
- Understand separation of validation concerns
- Implement async validation rules
- Return structured validation error responses
12.1 Data Annotation Validation (Review)
public class CommandMutateDto
{
[Required(ErrorMessage = "HowTo description is required")]
[MaxLength(250, ErrorMessage = "HowTo cannot exceed 250 characters")]
[MinLength(5, ErrorMessage = "HowTo must be at least 5 characters")]
public required string HowTo { get; init; }
[Required(ErrorMessage = "Command line is required")]
[RegularExpression(@"^[\w\s\-\.\/]+$",
ErrorMessage = "Command line contains invalid characters")]
public required string CommandLine { get; init; }
[Range(1, int.MaxValue, ErrorMessage = "Valid platform ID is required")]
public int PlatformId { get; set; }}
12.2 NEW: FluentValidation Introduction
dotnet add package FluentValidation.AspNetCore
// Program.cs
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
- Why FluentValidation:
- Separation of concerns
- More powerful validation logic
- Easier testing
- Conditional validation
- Custom error messages
- Async validation support
12.3 Installing and Configuring FluentValidation
using FluentValidation;
namespace CommandAPI.Validators;
public class CommandMutateDtoValidator : AbstractValidator<CommandMutateDto>
{
private readonly ICommandRepository _repository;
public CommandMutateDtoValidator(ICommandRepository repository)
{
_repository = repository;
RuleFor(x => x.HowTo)
.NotEmpty().WithMessage("HowTo description is required")
.MinimumLength(5).WithMessage("HowTo must be at least 5 characters")
.MaximumLength(250).WithMessage("HowTo cannot exceed 250 characters");
RuleFor(x => x.CommandLine)
.NotEmpty().WithMessage("Command line is required")
.Matches(@"^[\w\s\-\.\/]+$").WithMessage("Command line contains invalid characters")
.Must(NotContainDangerousCommands).WithMessage("Command contains potentially dangerous operations");
RuleFor(x => x.PlatformId)
.GreaterThan(0).WithMessage("Valid platform ID is required")
.MustAsync(PlatformExists).WithMessage("Platform does not exist");
}
private bool NotContainDangerousCommands(string commandLine)
{
var dangerousPatterns = new[] { "rm -rf", "del /f", "format" };
return !dangerousPatterns.Any(p => commandLine.Contains(p, StringComparison.OrdinalIgnoreCase));
}
private async Task<bool> PlatformExists(int platformId, CancellationToken cancellation)
{
var platform = await _repository.GetPlatformByIdAsync(platformId);
return platform != null;
}
}
12.4 Creating Validation Rules
public class PlatformMutateDtoValidator : AbstractValidator<PlatformMutateDto>
{
public PlatformMutateDtoValidator(ICommandRepository repository)
{
// Basic rules
RuleFor(x => x.PlatformName)
.NotEmpty()
.Length(2, 100)
.WithName("Platform Name"); // Custom property name in errors
// Conditional validation
When(x => !string.IsNullOrEmpty(x.PlatformName), () =>
{
RuleFor(x => x.PlatformName)
.Must(BeUniquePlatformName)
.WithMessage("Platform name already exists");
});
// Complex validation
RuleFor(x => x)
.Must(HaveValidConfiguration)
.WithMessage("Platform configuration is invalid");
}
private bool BeUniquePlatformName(string name)
{
// Check database for uniqueness
return true; // Implementation
}
}
12.5 Custom Validators
// Custom validator for API key format
public class ApiKeyValidator : AbstractValidator<string>
{
public ApiKeyValidator()
{
RuleFor(apiKey => apiKey)
.NotEmpty().WithMessage("API key is required")
.Length(72).WithMessage("API key must be exactly 72 characters")
.Matches(@"^[a-f0-9\-]+$").WithMessage("API key format is invalid");
}
}
// Usage in registration validator
public class KeyRegistrationValidator : AbstractValidator<KeyRegistrationMutateDto>
{
public KeyRegistrationValidator()
{
RuleFor(x => x.Description)
.NotEmpty()
.MaximumLength(500);
RuleFor(x => x.UserId)
.NotEmpty()
.Must(BeValidAuth0UserId).WithMessage("Invalid user ID format");
}
private bool BeValidAuth0UserId(string userId)
{
// Auth0 user IDs start with "auth0|" or "google-oauth2|" etc.
return userId.Contains('|');
}
}
12.6 REVISIT: Updating DTOs with Proper Validation
- Remove data annotations from DTOs (or keep for basic validation)
- Create FluentValidation validators for each DTO
- Register validators in DI container
- Test validation rules independently