Skip to content

Validation Pattern

Request validation using FluentValidation.

Overview

All incoming requests are validated via ASP.NET Core auto-validation before reaching the controller:

Request → ASP.NET Auto-Validation → Controller → MiniatR → 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");

Base Validators & Composition

Reusable validation rules can be applied via inheritance or composition:

Base Class Interface Purpose
CaptchaRequestValidator<T> ICaptchaRequest CAPTCHA token validation
PaginatedRequestValidator<T> IPaginatedRequest PageNumber/PageSize validation

Option 1: Inheritance (when no other base class is needed)

public sealed class GetActionHistoryRequestValidator
    : PaginatedRequestValidator<GetActionHistoryRequest>
{
    public GetActionHistoryRequestValidator()
    {
        // Pagination rules inherited automatically
        RuleFor(x => x.DateFrom)
            .LessThanOrEqualTo(x => x.DateTo)
            .When(x => x.DateFrom.HasValue && x.DateTo.HasValue);
    }
}

Option 2: Composition (when you need another base class)

public sealed class MyRequestValidator : SomeOtherBaseValidator<MyRequest>
{
    public MyRequestValidator()
    {
        // Add pagination rules via extension method
        this.AddPaginationRules();

        // Add other rules
        RuleFor(x => x.Name).NotEmpty();
    }
}

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 with ASP.NET Core integration:

services.AddValidatorsFromAssemblyContaining<LoginRequestValidator>(includeInternalTypes: true);
services.AddFluentValidationAutoValidation();