# FluentValidation in VSA Pipelines

> **Scope:** dotnet-vsa
> **Layer:** 0 (always load)
> **Keywords:** fluent-validation, validation, decorator, pipeline, vsa
> **Load When:** dotnet VSA projects

**Verified against:** .NET 10 + FluentValidation 11 (`AbstractValidator`/`RuleFor` API). Last-verified: 2026-05-20.

---

## Overview

FluentValidation integrates into VSA via the decorator pattern. Validators are auto-discovered per assembly, and the `ValidationDecorator<TRequest, TResponse>` runs all matching validators before the handler executes. Failed validation short-circuits to `Result.Failure` without touching the handler.

---

## Validator Structure

One validator per request DTO. Lives in the same feature folder as the handler:

```csharp
// Features/BookFeature/CreateBook/CreateBookValidator.cs
public class CreateBookValidator : AbstractValidator<CreateBookRequest>
{
    // TimeProvider injected, never DateTime.UtcNow read directly — see
    // backend-dotnet-testing (Rule 1): a validator that reads the real clock
    // cannot be tested deterministically with FakeTimeProvider.
    public CreateBookValidator(TimeProvider timeProvider)
    {
        RuleFor(c => c.Title)
            .NotEmpty().WithMessage("Title is required")
            .MaximumLength(200).WithMessage("Title must not exceed 200 characters");

        RuleFor(c => c.Author)
            .NotEmpty().WithMessage("Author is required")
            .MaximumLength(100).WithMessage("Author must not exceed 100 characters");

        RuleFor(c => c.ISBN)
            .NotEmpty().WithMessage("ISBN is required");

        RuleFor(c => c.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0");

        RuleFor(c => c.PublishedYear)
            .GreaterThan(1000).WithMessage("Published year must be a valid year")
            .LessThanOrEqualTo(timeProvider.GetUtcNow().Year)
            .WithMessage("Published year cannot be in the future");
    }
}
```

---

## Auto-Discovery

Register all validators from the assembly in `Program.cs`. This must happen **before** handler registration so the `ValidationDecorator` can resolve them:

```csharp
// Program.cs
builder.Services.AddValidatorsFromAssembly(typeof(CreateBookValidator).Assembly);
builder.Services.AddHandlersFromAssembly(typeof(Program).Assembly);
```

---

## ValidationDecorator Pattern

The decorator wraps every `IHandler<TRequest, TResponse>` via Scrutor. It collects all `IValidator<TRequest>` from DI, runs them in parallel, and short-circuits on failure:

```csharp
public sealed class ValidationDecorator<TRequest, TResponse>(
    IEnumerable<IValidator<TRequest>> validators,
    IHandler<TRequest, TResponse> innerHandler) : IHandler<TRequest, TResponse>
{
    public async Task<TResponse> HandleAsync(
        TRequest command, CancellationToken cancellationToken)
    {
        // No validators registered for this request — pass through
        if (!validators.Any())
        {
            return await innerHandler.HandleAsync(command, cancellationToken);
        }

        var context = new ValidationContext<TRequest>(command);

        // Run all validators in parallel
        ValidationFailure[] failures = (await Task.WhenAll(
                validators.Select(v => v.ValidateAsync(context, cancellationToken))))
            .SelectMany(r => r.Errors)
            .Where(f => f is not null)
            .ToArray();

        if (failures.Length == 0)
        {
            return await innerHandler.HandleAsync(command, cancellationToken);
        }

        // Short-circuit: convert to Result failure
        return CreateFailureResponse(failures);
    }
}
```

---

## FluentValidation to ValidationError Conversion

`ValidationFailure[]` from FluentValidation is converted to the Result pattern's `ValidationError`:

```csharp
private static ValidationError CreateValidationError(ValidationFailure[] validationFailures) =>
    new(validationFailures
        .Select(f => Error.Validation(f.ErrorCode, f.ErrorMessage))
        .ToArray());
```

This produces a `ValidationError` containing an array of typed `Error` records, each with `ErrorType.Validation`.

---

## Reflection for Typed Failure Responses

The decorator supports both `Result` and `Result<T>` via reflection. This is necessary because the generic `TResponse` type is not known at compile time:

```csharp
private static TResponse CreateFailureResponse(ValidationFailure[] failures)
{
    // Non-generic Result
    if (typeof(TResponse) == typeof(Result))
    {
        return (TResponse)(object)Result.Failure(CreateValidationError(failures));
    }

    // Generic Result<T> — use reflection to call Result.Failure<T>()
    if (typeof(TResponse).IsGenericType &&
        typeof(TResponse).GetGenericTypeDefinition() == typeof(Result<>))
    {
        var valueType = typeof(TResponse).GetGenericArguments()[0];
        var failureMethod = typeof(Result)
            .GetMethods()
            .First(m =>
                m.Name == nameof(Result.Failure) &&
                m.IsGenericMethodDefinition &&
                m.GetParameters().Length == 1);

        var typedFailure = failureMethod
            .MakeGenericMethod(valueType)
            .Invoke(null, [CreateValidationError(failures)]);

        return (TResponse)typedFailure!;
    }

    throw new InvalidOperationException(
        $"ValidationDecorator supports only Result and Result<T> responses.");
}
```

---

## RuleFor Chain Reference

Common FluentValidation rules used in VSA:

| Rule | Purpose |
|------|---------|
| `.NotEmpty()` | Non-null and non-whitespace for strings |
| `.MaximumLength(n)` | String max length |
| `.GreaterThan(n)` | Numeric minimum (exclusive) |
| `.LessThanOrEqualTo(n)` | Numeric maximum (inclusive) |
| `.Must(predicate)` | Custom inline validation |
| `.WithMessage("...")` | Human-readable error message |
| `.WithErrorCode("...")` | Machine-readable error code |
| `.When(condition)` | Conditional rule application |

---

## File Placement

```
Features/
  BookFeature/
    CreateBook/
      CreateBookRequest.cs      (or inline in handler)
      CreateBookResponse.cs     (or inline in handler)
      CreateBookHandler.cs
      CreateBookValidator.cs    <-- one validator per request
      CreateBookEndpoint.cs
```

---

## Anti-Patterns

| Do NOT | Do Instead |
|--------|------------|
| Validate inside the handler | Use `AbstractValidator<T>` + decorator |
| Throw `ValidationException` | Return `Result.Failure(ValidationError)` |
| Register validators after handlers | Register validators first: `AddValidatorsFromAssembly()` before `AddHandlersFromAssembly()` |
| Put validation logic in endpoints | Endpoints only do `Match()` — validation is automatic |
| Use `IValidateOptions<T>` | Use `AbstractValidator<T>` (FluentValidation, not Microsoft) |
| Skip `.WithMessage()` | Always provide user-facing messages |
| Use `RuleSet` grouping | One validator per request DTO — no rulesets needed in VSA |

---

*MORPH-SPEC by Polymorphism Tech*
