Saltar al contenido principal

Validators

Validator Lifecycle

loading...

AbstractValidator<T>

AbstractValidator<T> is the base class you must inherit to create any validator. It is where you define all rules using the RuleFor method.

Basic Structure

public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.TotalAmount).GreaterThan(0);
RuleFor(x => x.Items).NotEmptyCollection();
}
}

The where T : class constraint means you cannot create validators for value types (structs, int, etc.) directly.

Dependency Injection in the Constructor

Validators can receive dependencies in the constructor:

public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
private readonly IUserRepository _userRepository;

public CreateUserValidator(IUserRepository userRepository)
{
_userRepository = userRepository;

RuleFor(x => x.Email)
.NotEmpty()
.Email()
.MustAsync(async email =>
{
var exists = await _userRepository.ExistsByEmailAsync(email);
return !exists;
})
.WithMessage("A user with that email already exists.");
}
}

Important: If the validator receives services via DI, register it as Scoped when those services are Scoped (such as DbContext). See Dependency Injection.


RuleFor

RuleFor accepts an expression that selects the property to validate. It returns an IRuleBuilder<T, TProperty> that allows chaining rules.

RuleFor(x => x.Email)
.NotEmpty()
.Email()
.MaximumLength(320);

The expression can access nested properties:

// Direct property
RuleFor(x => x.Name).NotEmpty();

// Nested property
RuleFor(x => x.Address.Street).NotEmpty();

RuleForEach

RuleForEach validates each element of a collection. Error keys use indexes: "Items[0]", "Items[1]", etc.

RuleForEach(x => x.Lines)
.Must(line => line.Quantity > 0)
.WithMessage("The quantity must be greater than 0.")
.Must(line => line.UnitPrice > 0)
.WithMessage("The unit price must be greater than 0.");

RuleForEach with SetValidator

public class InvoiceLineValidator : AbstractValidator<InvoiceLine>
{
public InvoiceLineValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
RuleFor(x => x.UnitPrice).GreaterThan(0);
}
}

public class CreateInvoiceValidator : AbstractValidator<CreateInvoiceRequest>
{
public CreateInvoiceValidator()
{
RuleFor(x => x.Lines).NotEmptyCollection();
RuleForEach(x => x.Lines)
.SetValidator(new InvoiceLineValidator());
}
}

Errors appear as "Lines[0].ProductId", "Lines[1].UnitPrice", etc.


Include

Include copies all rules from another validator into the current validator. Useful for rule inheritance or for composing validators from separate parts.

public class ProductBaseValidator : AbstractValidator<Product>
{
public ProductBaseValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0);
}
}

public class CreateProductValidator : AbstractValidator<Product>
{
public CreateProductValidator()
{
Include(new ProductBaseValidator());
// Additional rules specific to creation
RuleFor(x => x.Stock).GreaterThanOrEqualTo(0);
RuleFor(x => x.Category).NotEmpty();
}
}

Caution: Include is not the same as SetValidator. Include takes rules from another validator for the same type T. SetValidator delegates validation of a nested property to a validator of a different type.


RuleSwitch — Case-based Conditional Validation

RuleSwitch lets you apply completely different sets of rules across multiple properties depending on the value of a discriminator field.

Syntax

RuleSwitch(x => x.DiscriminatorProperty)
.Case(value1, rules => { /* configure rules on 'rules' */ })
.Case(value2, rules => { /* configure rules on 'rules' */ })
.Default(rules => { /* fallback when no case matches */ });

Complete Example: Payment Validator

public class PaymentValidator : AbstractValidator<PaymentDto>
{
public PaymentValidator()
{
// This rule always applies, regardless of payment method
RuleFor(x => x.Amount).Positive();

RuleSwitch(x => x.Method)
.Case("credit_card", rules =>
{
rules.RuleFor(x => x.CardNumber).NotEmpty();
rules.RuleFor(x => x.Cvv).NotEmpty().MinimumLength(3).MaximumLength(4);
rules.RuleFor(x => x.CardHolder).NotEmpty();
})
.Case("bank_transfer", rules =>
{
rules.RuleFor(x => x.Iban).NotEmpty().Iban();
rules.RuleFor(x => x.BankName).NotEmpty();
})
.Case("paypal", rules =>
{
rules.RuleFor(x => x.PaypalEmail).NotEmpty().Email();
})
.Default(rules =>
{
rules.RuleFor(x => x.Reference).NotEmpty();
});
}
}

Behavior: Only One Case Executes

RuleSwitch evaluates the discriminator once and runs at most one case. Cases are not cumulative.


Validation Methods

Validate (synchronous)

ValidationResult result = validator.Validate(instance);

The synchronous method executes only synchronous rules. Use it only when you are certain there are no async rules.

ValidateAsync

// Without CancellationToken
ValidationResult result = await validator.ValidateAsync(instance);

// With CancellationToken
ValidationResult result = await validator.ValidateAsync(instance, cancellationToken);

Executes all rules, including async ones. This is the recommended method in ASP.NET Core applications.

ValidateParallelAsync

ValidationResult result = await validator.ValidateParallelAsync(instance);
ValidationResult result = await validator.ValidateParallelAsync(instance, cancellationToken);

Executes all async rules in parallel. Useful when there are multiple MustAsync calls that make independent database or external service calls.

Warning: Do not use ValidateParallelAsync if rules have side effects or dependencies between them.

ValidateAndThrow / ValidateAndThrowAsync

// Throws ValidationException if validation fails
validator.ValidateAndThrow(instance);
await validator.ValidateAndThrowAsync(instance);
await validator.ValidateAndThrowAsync(instance, cancellationToken);

Equivalent to ValidateAsync + IsValid check + throwing ValidationException. Useful in service layers where you prefer to propagate the exception.

See Exceptions for more details about ValidationException.


IValidator<T> — Interface for DI

Always inject IValidator<T> instead of the concrete implementation:

public interface IValidator<T>
{
ValidationResult Validate(T instance);
Task<ValidationResult> ValidateAsync(T instance);
Task<ValidationResult> ValidateAsync(T instance, CancellationToken ct);
Task<ValidationResult> ValidateParallelAsync(T instance);
Task<ValidationResult> ValidateParallelAsync(T instance, CancellationToken ct);
void ValidateAndThrow(T instance);
Task ValidateAndThrowAsync(T instance);
Task ValidateAndThrowAsync(T instance, CancellationToken ct);
}
// Correct: depends on the interface
public class UserController : ControllerBase
{
private readonly IValidator<CreateUserRequest> _validator;

public UserController(IValidator<CreateUserRequest> validator)
{
_validator = validator;
}
}

Global CascadeMode

By default, if a property has multiple rules and the first one fails, all the others are still evaluated. You can change this behavior globally:

public class StrictValidator : AbstractValidator<PaymentRequest>
{
protected override CascadeMode GlobalCascadeMode => CascadeMode.StopOnFirstFailure;

public StrictValidator()
{
RuleFor(x => x.CardNumber)
.NotEmpty()
.CreditCard();

RuleFor(x => x.ExpiryMonth)
.GreaterThan(0)
.LessThanOrEqualTo(12);
}
}

See CascadeMode for the full comparison.


Next Steps