Skip to content

Validation Pattern

Request validation using FluentValidation.

Overview

All incoming requests are validated before reaching the handler:

Request → ValidationBehavior → Handler
        Validation errors? → 400 Bad Request

Validator Structure

public sealed class CreateMarkerRequestValidator
    : AbstractValidator<CreateMarkerRequest>
{
    public CreateMarkerRequestValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty()
            .MaximumLength(200);

        RuleFor(x => x.Latitude)
            .InclusiveBetween(-90, 90);

        RuleFor(x => x.Longitude)
            .InclusiveBetween(-180, 180);

        RuleFor(x => x.Rating)
            .InclusiveBetween(1, 5)
            .When(x => x.Rating.HasValue);
    }
}

Common Rules

Rule Use For
NotEmpty() Required strings, collections
NotNull() Required reference types
MaximumLength(n) String length limits
InclusiveBetween(a, b) Numeric ranges
EmailAddress() Email format
Must(predicate) Custom validation

Conditional Validation

RuleFor(x => x.EndDate)
    .GreaterThan(x => x.StartDate)
    .When(x => x.EndDate.HasValue);

Custom Rules

RuleFor(x => x.CategoryId)
    .MustAsync(async (id, ct) =>
        await context.Categories.AnyAsync(c => c.CategoryId == id, ct))
    .WithMessage("Category does not exist");

Error Response

Validation errors return 400 with field-specific messages:

{
  "code": "VALIDATION_ERROR",
  "errors": {
    "title": ["Title is required"],
    "latitude": ["Latitude must be between -90 and 90"]
  }
}

Registration

Validators are auto-registered via assembly scanning:

services.AddValidatorsFromAssembly(typeof(ApplicationAssembly).Assembly);