12. Model Validation

  1. 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