dotnet-cqrs/docs/core-features/validation
2025-12-11 01:18:24 -05:00
..
custom-validation.md this is a mess 2025-12-11 01:18:24 -05:00
fluentvalidation-setup.md this is a mess 2025-12-11 01:18:24 -05:00
grpc-validation.md this is a mess 2025-12-11 01:18:24 -05:00
http-validation.md this is a mess 2025-12-11 01:18:24 -05:00
README.md this is a mess 2025-12-11 01:18:24 -05:00

Validation Overview

Input validation ensures data integrity and provides clear error messages to clients.

What is Validation?

Validation is the process of verifying that commands and queries contain valid data before processing. The framework integrates with FluentValidation to provide:

  • Declarative validation rules - Define rules with fluent syntax
  • Automatic validation - Execute before handler invocation
  • Structured error responses - RFC 7807 (HTTP) or Google Rich Error Model (gRPC)
  • Async validation - Database lookups, external API calls
  • Reusable validators - Share validation logic across commands/queries
  • Custom validation - Extend with custom rules

Validation Flow

┌─────────────┐
│   Request   │
└──────┬──────┘
       │
       ▼
┌──────────────────┐
│  Model Binding   │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐    Validation    ┌────────────────┐
│    Validator     │─────fails────────▶│  Error Response│
└──────┬───────────┘                   └────────────────┘
       │
       │ Validation passes
       ▼
┌──────────────────┐
│  Authorization   │
└──────┬───────────┘
       │
       ▼
┌──────────────────┐
│     Handler      │
└──────────────────┘

Quick Example

Define Validator

using FluentValidation;

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("Name is required")
            .MaximumLength(100)
            .WithMessage("Name must not exceed 100 characters");

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .WithMessage("Valid email address is required");

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18)
            .WithMessage("User must be at least 18 years old");
    }
}

Register Validator

builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();

Automatic Validation

Validation happens automatically before the handler executes. If validation fails, the framework returns structured error responses without invoking the handler.

HTTP vs gRPC Validation

HTTP (RFC 7807 Problem Details)

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["Name is required"],
    "Email": ["Valid email address is required"],
    "Age": ["User must be at least 18 years old"]
  }
}

gRPC (Google Rich Error Model)

google.rpc.Status {
  code: 3  // INVALID_ARGUMENT
  message: "Validation failed"
  details: [
    google.rpc.BadRequest {
      field_violations: [
        { field: "name", description: "Name is required" },
        { field: "email", description: "Valid email address is required" },
        { field: "age", description: "User must be at least 18 years old" }
      ]
    }
  ]
}

Validation Documentation

FluentValidation Setup

Setting up validators:

  • Installing FluentValidation
  • Creating validators
  • Registering validators
  • Common validation rules

HTTP Validation

HTTP-specific validation:

  • RFC 7807 Problem Details
  • ASP.NET Core integration
  • Model state errors
  • Custom error responses

gRPC Validation

gRPC-specific validation:

  • Google Rich Error Model
  • Field violations
  • Error details
  • Client error handling

Custom Validation

Advanced validation scenarios:

  • Custom validators
  • Async validation
  • Database validation
  • Cross-property validation
  • Conditional validation

Common Validation Rules

Required Fields

RuleFor(x => x.Name)
    .NotEmpty()
    .WithMessage("Name is required");

String Length

RuleFor(x => x.Description)
    .MaximumLength(500)
    .WithMessage("Description must not exceed 500 characters");

RuleFor(x => x.Username)
    .MinimumLength(3)
    .MaximumLength(20);

Email Validation

RuleFor(x => x.Email)
    .EmailAddress()
    .WithMessage("Valid email address is required");

Numeric Ranges

RuleFor(x => x.Age)
    .GreaterThanOrEqualTo(0)
    .LessThanOrEqualTo(150);

RuleFor(x => x.Quantity)
    .InclusiveBetween(1, 100);

Regular Expressions

RuleFor(x => x.PhoneNumber)
    .Matches(@"^\+?[1-9]\d{1,14}$")
    .WithMessage("Invalid phone number format");

Custom Predicates

RuleFor(x => x.StartDate)
    .Must(date => date > DateTime.UtcNow)
    .WithMessage("Start date must be in the future");

Async Validation

Validation can be asynchronous for database lookups or external API calls:

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    private readonly IUserRepository _userRepository;

    public CreateUserCommandValidator(IUserRepository userRepository)
    {
        _userRepository = userRepository;

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(BeUniqueEmail)
            .WithMessage("Email address is already in use");
    }

    private async Task<bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
    {
        var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken);
        return existingUser == null;
    }
}

Validation Severity

FluentValidation supports different severity levels:

RuleFor(x => x.Name)
    .NotEmpty()
    .WithMessage("Name is required")
    .WithSeverity(Severity.Error);

RuleFor(x => x.Description)
    .MaximumLength(500)
    .WithMessage("Description is longer than recommended")
    .WithSeverity(Severity.Warning);

RuleFor(x => x.Tags)
    .Must(tags => tags.Count <= 10)
    .WithMessage("Consider using fewer tags for better organization")
    .WithSeverity(Severity.Info);

Best Practices

DO

  • Validate all user input
  • Use descriptive error messages
  • Validate at the boundary (commands/queries)
  • Use async validation for database checks
  • Keep validators focused and single-purpose
  • Reuse validators across similar commands
  • Test validators independently

DON'T

  • Don't validate in handlers (validate earlier)
  • Don't throw exceptions for validation errors
  • Don't skip validation for internal commands
  • Don't perform business logic in validators
  • Don't validate domain entities (validate DTOs/commands)
  • Don't return generic error messages

Testing Validators

using FluentValidation.TestHelper;

[Fact]
public void Should_Require_Name()
{
    var validator = new CreateUserCommandValidator();
    var command = new CreateUserCommand { Name = "" };

    var result = validator.TestValidate(command);

    result.ShouldHaveValidationErrorFor(x => x.Name)
        .WithErrorMessage("Name is required");
}

[Fact]
public void Should_Reject_Invalid_Email()
{
    var validator = new CreateUserCommandValidator();
    var command = new CreateUserCommand { Email = "invalid-email" };

    var result = validator.TestValidate(command);

    result.ShouldHaveValidationErrorFor(x => x.Email);
}

[Fact]
public void Should_Pass_Valid_Command()
{
    var validator = new CreateUserCommandValidator();
    var command = new CreateUserCommand
    {
        Name = "John Doe",
        Email = "john@example.com",
        Age = 25
    };

    var result = validator.TestValidate(command);

    result.ShouldNotHaveAnyValidationErrors();
}

What's Next?

See Also