Elarion
Core Concepts

Validators

Validators use FluentValidation, are grouped by module namespace, and are registered by the generator.

Validators use FluentValidation and are grouped by namespace under their module. Place a validator beside the handler it validates and the generator registers it.

using FluentValidation;

namespace MyApp.Application.Modules.Clients.Handlers.CreateClient;

public sealed class CreateClientValidator : AbstractValidator<CreateClient.Command> {
    public CreateClientValidator() {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
    }
}

When [assembly: UseElarion] or [assembly: GenerateModuleValidators] is present, Elarion emits Add{ModuleName}Validators() into the module namespace, which the module's ConfigureServices calls.

How validators run

Registration alone does not enforce validation. Validators are invoked by a validation decorator that you add to a pipeline. The decorator resolves all IValidator<TRequest> instances for a request, runs them, and converts any failures into a Result<T>.Failure(AppError.Validation(...)) before the handler executes:

public sealed class ValidationDecorator<TRequest, TResponse>(
    IHandler<TRequest, TResponse> inner,
    IEnumerable<IValidator<TRequest>> validators
) : IHandler<TRequest, TResponse> {
    public async ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken ct) {
        var failures = new List<string>();
        foreach (var validator in validators) {
            var result = await validator.ValidateAsync(request, ct);
            failures.AddRange(result.Errors.Select(e => e.ErrorMessage));
        }

        if (failures.Count == 0) {
            return await inner.HandleAsync(request, ct);
        }

        return ResultFactory.Failure<TResponse>(
            AppError.Validation(string.Join("; ", failures), failures));
    }
}

This keeps validation declarative (rules next to the command) and composable (one decorator applies it across every handler in a pipeline). The structured AppError.Validation(message, errors) overload carries the individual messages for the transport to surface.

Conventions

  • The validator inherits AbstractValidator<T> where T is the request type.
  • It lives under the module namespace so the module owns its registration.

If a validator is outside the module namespace or does not inherit AbstractValidator<T>, it is not registered. See Troubleshooting.

On this page