Section 4 of 7

Validation

🎯 What You'll Learn

  • Why validation matters
  • Data Annotations attributes
  • Automatic validation with [ApiController]
  • Custom validation attributes
  • FluentValidation library
  • Returning validation errors

Why Validation?

Validation ensures data integrity by checking that input meets business rules before processing.

  • Security: Prevent malicious input
  • Data integrity: Ensure valid data in database
  • User experience: Provide clear error messages
  • Business rules: Enforce domain constraints

Data Annotations

Data Annotations are attributes that define validation rules on model properties.

Common Validation Attributes

Attribute Description Example
[Required] Property must have a value [Required]
[StringLength] String length constraints [StringLength(100)]
[MinLength] Minimum length [MinLength(3)]
[MaxLength] Maximum length [MaxLength(50)]
[Range] Numeric range [Range(1, 100)]
[EmailAddress] Valid email format [EmailAddress]
[Phone] Valid phone number [Phone]
[Url] Valid URL [Url]
[RegularExpression] Regex pattern [RegularExpression(@"^\d{5}$")]
[Compare] Compare with another property [Compare("Password")]

Basic Validation Example

DTOs/CreateProductDto.cs C#
public class CreateProductDto
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(100, MinimumLength = 3, 
        ErrorMessage = "Name must be between 3 and 100 characters")]
    public string Name { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 1000000, ErrorMessage = "Price must be between $0.01 and $1,000,000")]
    public decimal Price { get; set; }

    [StringLength(500, ErrorMessage = "Description cannot exceed 500 characters")]
    public string? Description { get; set; }

    [Required]
    [Range(0, int.MaxValue, ErrorMessage = "Quantity must be non-negative")]
    public int Quantity { get; set; }

    [EmailAddress(ErrorMessage = "Invalid email address")]
    public string? SupplierEmail { get; set; }
}

Automatic Validation with [ApiController]

The [ApiController] attribute automatically validates models and returns 400 Bad Request with validation errors.

Automatic Validation C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public ActionResult CreateProduct(CreateProductDto dto)
    {
        // No need to check ModelState.IsValid!
        // [ApiController] does it automatically
        
        // If validation fails, returns 400 automatically
        return Ok("Product created");
    }
}

Validation Error Response

400 Bad Request Response JSON
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": [
      "Product name is required"
    ],
    "Price": [
      "Price must be between $0.01 and $1,000,000"
    ]
  }
}

Manual Validation (Without [ApiController])

Manual ModelState Check C#
[HttpPost]
public ActionResult CreateProduct(CreateProductDto dto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // Process valid data
    return Ok("Product created");
}

Custom Validation Attributes

Custom Validation Attribute C#
public class FutureDateAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(
        object? value,
        ValidationContext validationContext)
    {
        if (value is DateTime date)
        {
            if (date <= DateTime.Now)
            {
                return new ValidationResult("Date must be in the future");
            }
        }

        return ValidationResult.Success;
    }
}

// Usage
public class CreateOrderDto
{
    [FutureDate]
    public DateTime DeliveryDate { get; set; }
}

Class-Level Validation

IValidatableObject C#
public class CreateProductDto : IValidatableObject
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public decimal DiscountPrice { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (DiscountPrice >= Price)
        {
            yield return new ValidationResult(
                "Discount price must be less than regular price",
                new[] { nameof(DiscountPrice) });
        }

        if (Name.Contains("test", StringComparison.OrdinalIgnoreCase))
        {
            yield return new ValidationResult(
                "Product name cannot contain 'test'",
                new[] { nameof(Name) });
        }
    }
}

FluentValidation

FluentValidation is a popular library for building strongly-typed validation rules.

Installation

Package Installation Bash
dotnet add package FluentValidation.AspNetCore

Creating a Validator

Validators/CreateProductDtoValidator.cs C#
public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
    public CreateProductDtoValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name is required")
            .Length(3, 100).WithMessage("Name must be between 3 and 100 characters")
            .Must(name => !name.Contains("test", StringComparison.OrdinalIgnoreCase))
            .WithMessage("Product name cannot contain 'test'");

        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0")
            .LessThanOrEqualTo(1000000).WithMessage("Price cannot exceed $1,000,000");

        RuleFor(x => x.Description)
            .MaximumLength(500).WithMessage("Description cannot exceed 500 characters")
            .When(x => x.Description != null);

        RuleFor(x => x.Quantity)
            .GreaterThanOrEqualTo(0).WithMessage("Quantity must be non-negative");

        RuleFor(x => x.SupplierEmail)
            .EmailAddress().WithMessage("Invalid email address")
            .When(x => !string.IsNullOrEmpty(x.SupplierEmail));
    }
}

Register FluentValidation

Program.cs C#
using FluentValidation;
using FluentValidation.AspNetCore;

builder.Services.AddControllers();

// Register FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductDtoValidator>();

Complete InvenTrack Example

DTOs/CreateProductDto.cs C#
public class CreateProductDto
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(100, MinimumLength = 3)]
    public string Name { get; set; } = string.Empty;

    [Required]
    [Range(0.01, 1000000)]
    public decimal Price { get; set; }

    [StringLength(500)]
    public string? Description { get; set; }

    [Required]
    [Range(0, int.MaxValue)]
    public int Quantity { get; set; }

    [Required]
    [StringLength(50)]
    public string Category { get; set; } = string.Empty;

    [Url]
    public string? ImageUrl { get; set; }
}
Controllers/ProductsController.cs C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public ActionResult<Product> CreateProduct(CreateProductDto dto)
    {
        // Validation happens automatically with [ApiController]
        // If invalid, returns 400 Bad Request with errors

        var product = new Product
        {
            Id = 123,
            Name = dto.Name,
            Price = dto.Price,
            Description = dto.Description,
            Quantity = dto.Quantity
        };

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public ActionResult<Product> GetProduct(int id)
    {
        return Ok(new Product { Id = id });
    }
}

Best Practices

  • Validate early: Validate at API boundary
  • Use DTOs: Don't validate domain models directly
  • Clear messages: Provide helpful error messages
  • FluentValidation: Consider for complex validation
  • Custom attributes: For reusable validation logic
  • IValidatableObject: For cross-property validation
  • Client-side validation: Validate on client too (UX)
  • Security: Never trust client input

Key Takeaways

  • Data Annotations: Attribute-based validation
  • [ApiController]: Automatic validation
  • ModelState: Contains validation errors
  • Custom attributes: Extend ValidationAttribute
  • IValidatableObject: Class-level validation
  • FluentValidation: Strongly-typed validation rules
  • 400 Bad Request: Standard validation error response
  • Error messages: Customize for better UX
🎯 Next Steps

You now understand validation! In the next section, we'll explore Minimal APIs—a lightweight alternative to controllers for building simple, fast APIs with less ceremony.