Pitasoft.Validation 0.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Pitasoft.Validation --version 0.0.1
                    
NuGet\Install-Package Pitasoft.Validation -Version 0.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pitasoft.Validation" Version="0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pitasoft.Validation" Version="0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Pitasoft.Validation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pitasoft.Validation --version 0.0.1
                    
#r "nuget: Pitasoft.Validation, 0.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pitasoft.Validation@0.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pitasoft.Validation&version=0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Pitasoft.Validation&version=0.0.1
                    
Install as a Cake Tool

Pitasoft.Validation

Build Status Code Coverage NuGet Version NuGet Downloads License: MIT .NET Versions

A flexible and high-performance C# validation library designed to validate objects using multiple strategies, including Data Annotations, a fluent API, and custom checkers. Built on top of Pitasoft.Error for consistent error handling.


English

Features

  • Multiple validation strategies — Data Annotations, fluent API, and custom IChecker<T> implementations.
  • Asynchronous validationMustAsync rules with full CancellationToken support throughout the call chain.
  • Async conditional rulesWhenAsync / UnlessAsync for conditions that require async operations.
  • Property-to-property comparison — Compare a property against the value of another property using lambda expressions.
  • Hierarchical validation — Register child validators to validate nested object graphs.
  • Collection validation — Validate each element of a collection with indexed error paths (Items[0].Name).
  • Conditional validationWhen / Unless for context-dependent rules.
  • Stop on first failure — Per-property option to stop after the first failing rule.
  • Reusable validators — Inherit from Validator<T> or ValidatorAsync<T> to define reusable validator classes.
  • Error codes — Attach optional codes to rules for programmatic identification.
  • Exception-safe rule evaluation — Exceptions thrown inside rules are caught and reported as validation errors.
  • High performance — Compiled lambda expressions, cached property metadata, and cached compiled regex patterns.
  • Pitasoft.Error integration — All results are returned as ErrorCollection.
  • Strongly typed — Full generics and lambda expressions; no magic strings.

Installation

dotnet add package Pitasoft.Validation

Requires Pitasoft.Error.


Core Concepts

There are two main validator roles:

Class Description
Validator<T> Synchronous validator. Use when all rules are synchronous.
ValidatorAsync<T> Asynchronous validator. Prefer *Async methods. Sync methods delegate to checkers — they work if all registered checkers support sync; async-only checkers throw AsyncRuleInSyncValidationException by default, or skip async rules when AsyncRuleBehavior.Skip is set.

Rules are defined using checkers:

Class Description
DataAnnotationsChecker<T> Reads ValidationAttribute annotations from the model.
RuleChecker<T> Fluent API for synchronous rules.
RuleCheckerAsync<T> Fluent API for both synchronous and asynchronous rules.

The public interfaces follow a two-level contract:

Interface Purpose
IValidator<T> Consumer contract — inject this in services, use for mocking and decorators.
IValidatorAsync<T> Async consumer contract — extends IValidator<T> with *Async methods. Implementations may also support synchronous validation through the inherited API, depending on their registered checkers.
IValidatorBuilder<T> Construction contract — extends IValidator<T>, adds RegisterChecker. Use when subclassing.

Getting Started

1. Data Annotations Validation

The simplest way to start is to leverage existing attributes on your models.

using System.ComponentModel.DataAnnotations;
using Pitasoft.Validation;
using Pitasoft.Validation.DataAnnotations;

public class User
{
    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string Name { get; set; }

    [Range(18, 99)]
    public int Age { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

// Using a Validator with DataAnnotationsChecker
var validator = new Validator<User>(new DataAnnotationsChecker<User>());
var errors = validator.ValidateObject(user);

if (errors.HasErrors)
{
    foreach (var entry in errors)
        Console.WriteLine($"{entry.Key}: {string.Join(", ", entry.Value)}");
}

// Or via extension method (shortest path)
using Pitasoft.Validation.Extensions;

var errors = user.ValidateWithAttributes();
2. Fluent API Validation

Define rules programmatically without modifying your model.

using Pitasoft.Validation.Rules;
using Pitasoft.Validation.Extensions;

var checker = new RuleChecker<User>();

checker.For(u => u.Name)
    .StopOnFirstFailure()
    .NotNullOrEmpty("Name is required")
    .MinLength(2, "Name is too short (min {1} chars)")    // {1} = value
    .MaxLength(50, "{0} must not exceed 50 characters")   // {0} = display name
    .Matches(@"^[a-zA-Z\s]+$", "Name must contain only letters");

checker.For(u => u.Age)
    .Range(18, 99, "Age must be between 18 and 99");

checker.For(u => u.Email)
    .NotNullOrEmpty("Email is required")
    .Email("Invalid email format");

var errors = user.ValidateWithChecker(checker);

// Or inline without creating the checker explicitly
var errors = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Name is required").MaxLength(50, "Too long");
    v.For(u => u.Age).Range(18, 99, "Invalid age");
});
3. Reusable Validator Classes

Inherit from Validator<T> to encapsulate reusable validation logic in a class.

public class UserValidator : Validator<User>
{
    public UserValidator()
    {
        var checker = new RuleChecker<User>();

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("Name is required", code: "NAME_REQUIRED")
            .MinLength(2, "Name is too short", code: "NAME_MIN_LENGTH")
            .MaxLength(50, "Name is too long", code: "NAME_MAX_LENGTH");

        checker.For(u => u.Age)
            .Range(18, 99, "Age must be between 18 and 99", code: "AGE_OUT_OF_RANGE");

        RegisterChecker(checker);
    }
}

// Usage
var validator = new UserValidator();
var errors = validator.ValidateObject(user);

// Helper methods
bool isValid = validator.IsValid(user);

if (!validator.TryValidate(user, out var errors))
{
    // handle errors
}
4. Async Validation

Use ValidatorAsync<T> when any rule requires async operations (e.g. database lookups). Prefer the *Async variants. Synchronous methods still delegate to the registered checkers, so sync validation may fully work, partially validate, or throw AsyncRuleInSyncValidationException depending on the sync capabilities of those checkers and their AsyncRuleBehavior.

public class UserValidator : ValidatorAsync<User>
{
    public UserValidator(IUserRepository repo)
    {
        var checker = new RuleCheckerAsync<User>();

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("Name is required")
            .MaxLength(50, "Name is too long")
            // Async rule with CancellationToken
            .MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name, ct),
                "Name is already taken", code: "NAME_TAKEN");

        checker.For(u => u.Email)
            .NotNullOrEmpty("Email is required")
            .Email("Invalid email format")
            .MustAsync(async u => !await repo.EmailExistsAsync(u.Email),
                "Email is already in use");

        RegisterChecker(checker);
    }
}

// Usage — always prefer the async path
var validator = new UserValidator(repo);
var errors = await validator.ValidateObjectAsync(user);

// With CancellationToken
var errors = await validator.ValidateObjectAsync(user, cancellationToken);

// Helper methods
bool isValid = await validator.IsValidAsync(user, cancellationToken);
var (isValid, errors) = await validator.TryValidateAsync(user, cancellationToken);

Sync validation in ValidatorAsync<T>: Sync methods (ValidateObject, ValidateProperty, …) are available and delegate to each registered checker. Depending on those checkers, sync validation may succeed completely, perform partial sync validation, or throw AsyncRuleInSyncValidationException. AsyncRuleBehavior.Skip is the mechanism that enables partial sync validation for async-capable rule checkers.

var checker = new RuleCheckerAsync<User>();
checker.For(u => u.Name)
    .NotNullOrEmpty("Name is required")              // sync rule
    .MustAsync(async u => ..., "Name already taken"); // async rule

// Skip async rules during sync validation (e.g. for real-time UI feedback)
checker.AsyncRuleBehavior = AsyncRuleBehavior.Skip;
var errors = validator.ValidateObject(user); // runs sync rules only
5. Validate Specific Properties

Validate a subset of properties, useful for real-time form validation.

// Synchronous
var errors = validator.ValidateProperties(user, nameof(User.Name), nameof(User.Email));
var errors = validator.ValidateProperty(user, nameof(User.Name));

// Asynchronous
var errors = await validator.ValidatePropertiesAsync(user, nameof(User.Name));
var errors = await validator.ValidatePropertyAsync(user, nameof(User.Email), cancellationToken);

// With lambda expression (extension method)
var errors = validator.ValidateProperty(user, u => u.Name);
var errors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Fluent API Reference

String Rules
Method Description
NotNullOrEmpty(msg) Value is not null, empty, or whitespace
MinLength(min, msg) String length ≥ min
MaxLength(max, msg) String length ≤ max
Length(exact, msg) String length equals exact
LengthBetween(min, max, msg) String length within range
Matches(pattern, msg) Matches a regular expression (pattern is cached)
Email(msg) Valid email address format
Null and Equality Rules
Method Description
NotNull(msg) Value is not null
Null(msg) Value is null
Equal(value, msg) Value equals the given literal
Equal(x => x.Other, msg) Value equals the value of another property
NotEqual(value, msg) Value does not equal the given literal
NotEqual(x => x.Other, msg) Value does not equal the value of another property
Comparison and Range Rules
Method Description
Range(min, max, msg) Value is within range (inclusive)
GreaterThan(min, msg) Value > literal
GreaterThan(x => x.Other, msg) Value > other property
GreaterThanOrEqual(min, msg) Value ≥ literal
GreaterThanOrEqual(x => x.Other, msg) Value ≥ other property
LessThan(max, msg) Value < literal
LessThan(x => x.Other, msg) Value < other property
LessThanOrEqual(max, msg) Value ≤ literal
LessThanOrEqual(x => x.Other, msg) Value ≤ other property
Collection Rules
Method Description
NotEmpty(msg) Collection is not null and has at least one element
MinItems(min, msg) Collection has at least min elements
MaxItems(max, msg) Collection has at most max elements
Custom and Conditional Rules
Method Description
Must(predicate, msg) Custom synchronous rule; receives the full object
MustAsync(predicate, msg) Custom async rule (Func<T, Task<bool>>)
MustAsync((t, ct) => ..., msg) Custom async rule with CancellationToken
When(condition, configure) Apply rules only when sync condition is true
Unless(condition, configure) Apply rules only when sync condition is false
WhenAsync(condition, configure) Apply rules only when async condition is true
UnlessAsync(condition, configure) Apply rules only when async condition is false
StopOnFirstFailure() Stop after the first failing rule for this property

All methods accept an optional code parameter:

checker.For(u => u.Name)
    .NotNullOrEmpty("Name is required", code: "NAME_REQUIRED");
Error Message Placeholders
Placeholder Value
{0} Display name (from [Display(Name = "...")] or property name)
{1} Current property value

Advanced Usage

Conditional Validation (When / Unless)
var checker = new RuleChecker<User>();

// Rule applies only when condition is true
checker.For(u => u.CompanyName)
    .When(u => u.IsCompany,
        b => b.NotNullOrEmpty("Company name is required for companies"));

// Rule applies only when condition is false
checker.For(u => u.PersonalId)
    .Unless(u => u.IsCompany,
        b => b.NotNullOrEmpty("Personal ID is required for individuals"));
Async Conditional Validation (WhenAsync / UnlessAsync)

Use WhenAsync on a RuleCheckerAsync<T> when the condition itself requires an async operation.

var checker = new RuleCheckerAsync<User>();

checker.For(u => u.DiscountCode)
    .WhenAsync(async u => await featureService.IsEnabledAsync("discounts"),
        b => b.MustAsync(async u => await discountRepo.IsValidAsync(u.DiscountCode),
            "Discount code is not valid"));
Property-to-Property Comparison

Compare a property's value against another property on the same object.

var checker = new RuleChecker<Order>();

// EndDate must be after StartDate
checker.For(o => o.EndDate)
    .GreaterThan(o => o.StartDate, "End date must be after start date");

// Confirm password must match password
checker.For(u => u.ConfirmPassword)
    .Equal(u => u.Password, "Passwords do not match");

// MaxItems must be >= MinItems
checker.For(o => o.MaxItems)
    .GreaterThanOrEqual(o => o.MinItems, "Max must be ≥ min");
Cross-Property Custom Rules

Must receives the full object instance, enabling multi-field validation:

checker.For(u => u.YearsOfExperience)
    .Must(u => u.YearsOfExperience >= u.MinRequiredExperience,
        "Years of experience ({1}) must be at least the minimum required");
Stop on First Failure

Stop rule evaluation for a property after the first failure, avoiding redundant error messages:

checker.For(u => u.Name)
    .StopOnFirstFailure()        // only the first failing rule is reported
    .NotNullOrEmpty("Name is required")
    .MinLength(2, "Name is too short")
    .MaxLength(50, "Name is too long");
Child Validators (Nested Objects)

Register a validator for a nested property. Errors are prefixed with the property path.

var addressValidator = new Validator<Address>(new DataAnnotationsChecker<Address>());

var orderValidator = new Validator<Order>();
orderValidator.RegisterChildValidator(o => o.ShippingAddress, addressValidator);

var errors = orderValidator.ValidateObject(order);
// Error keys: "ShippingAddress.City", "ShippingAddress.PostalCode", etc.

Child validators can also be async:

var addressValidator = new ValidatorAsync<Address>();
// configure addressValidator...

var orderValidator = new ValidatorAsync<Order>();
orderValidator.RegisterChildValidator(o => o.ShippingAddress, addressValidator);

var errors = await orderValidator.ValidateObjectAsync(order, cancellationToken);
Collection Validation (ForEach)

Validate each element of a collection individually. Errors include the element index.

var validator = new Validator<Order>();
validator.ForEach(o => o.Lines, checker =>
{
    checker.For(l => l.Quantity).GreaterThan(0, "Quantity must be greater than zero");
    checker.For(l => l.ProductId).NotNullOrEmpty("Product is required");
});

var errors = validator.ValidateObject(order);
// Error keys: "Lines[0].Quantity", "Lines[1].ProductId", etc.

With async rules:

var validator = new ValidatorAsync<Order>();
validator.ForEach(o => o.Lines, (RuleCheckerAsync<OrderLine> checker) =>
{
    checker.For(l => l.ProductId)
        .NotNullOrEmpty("Product is required")
        .MustAsync(async (l, ct) => await productRepo.ExistsAsync(l.ProductId, ct),
            "Product does not exist");
});

var errors = await validator.ValidateObjectAsync(order, cancellationToken);
Combining DataAnnotations and Fluent Rules

Register multiple checkers on the same validator:

var ruleChecker = new RuleChecker<User>();
ruleChecker.For(u => u.Name)
    .Must(u => !u.Name.Contains("Admin", StringComparison.OrdinalIgnoreCase),
        "Name cannot contain 'Admin'");

// DataAnnotationsChecker validates [Required], [StringLength], etc.
// RuleChecker adds the custom "no Admin" rule on top.
var validator = new Validator<User>(new DataAnnotationsChecker<User>(), ruleChecker);
var errors = validator.ValidateObject(user);
Error Codes

Attach an error code to any rule for programmatic handling:

checker.For(u => u.Name)
    .NotNullOrEmpty("Name is required", code: "NAME_REQUIRED")
    .MaxLength(50, "Name is too long", code: "NAME_MAX_LENGTH");

Extension Methods

Object Extensions (ObjectExtensions)

Directly validate an object without creating a validator instance:

using Pitasoft.Validation.Extensions;

// Data Annotations
var errors = user.ValidateWithAttributes();
bool isValid = user.TryValidateWithAttributes(out var errors);

// Sync checker
var errors = user.ValidateWithChecker(checker);
bool isValid = user.TryValidateWithChecker(checker, out var errors);

// Sync inline rules
var errors = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Required");
    v.For(u => u.Age).Range(18, 99, "Invalid age");
});
bool isValid = user.TryValidateWithValidator(configure, out var errors);

// Async checker (with CancellationToken)
var errors = await user.ValidateWithCheckerAsync(asyncChecker, cancellationToken);

// Async inline rules (with CancellationToken)
var errors = await user.ValidateWithValidatorAsync(v =>
{
    v.For(u => u.Email).MustAsync(async u => !await repo.EmailExistsAsync(u.Email), "Email in use");
}, cancellationToken);
Validator Extensions (ValidatorExtensions)
using Pitasoft.Validation.Extensions;

// Sync
bool isValid = validator.IsValid(user);
bool isValid = validator.TryValidate(user, out var errors);

// Async (IValidatorAsync<T>)
bool isValid = await validator.IsValidAsync(user, cancellationToken);
(bool isValid, ErrorCollection errors) = await validator.TryValidateAsync(user, cancellationToken);

// Validate a single property via lambda
var errors = validator.ValidateProperty(user, u => u.Name);
var errors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Custom Checkers

The recommended way to create custom checkers is to inherit from the abstract base classes. They provide a default CheckObject / CheckObjectAsync loop so you only implement the per-property logic.

Synchronous checker — CheckerBase<T>

Override GetProperties and Check. CheckObject is derived automatically.

public class TaxIdFormatChecker : CheckerBase<Company>
{
    public override IEnumerable<string> GetProperties() => [nameof(Company.TaxId)];

    public override ErrorCollection? Check(Company? instance, string propertyName, string displayName)
    {
        if (instance?.TaxId is null) return null;
        if (IsValidFormat(instance.TaxId)) return null;
        var errors = new ErrorCollection();
        errors.Add(propertyName, $"{displayName} has an invalid format.");
        return errors;
    }

    private static bool IsValidFormat(string taxId) => taxId.Length == 9;
}

// Register it
var validator = new Validator<Company>(new TaxIdFormatChecker());
var errors = validator.ValidateObject(company);
Asynchronous checker — CheckerAsyncBase<T>

Override GetProperties and CheckAsync. CheckObjectAsync is derived automatically. Synchronous entry points (Check / CheckObject) throw AsyncRuleInSyncValidationException — register inside a ValidatorAsync<T> and use the async validation path.

public class TaxIdChecker : CheckerAsyncBase<Company>
{
    private readonly ITaxService _taxService;

    public TaxIdChecker(ITaxService taxService) => _taxService = taxService;

    public override IEnumerable<string> GetProperties() => [nameof(Company.TaxId)];

    public override async Task<ErrorCollection?> CheckAsync(Company? instance, string propertyName,
        string displayName, CancellationToken cancellationToken = default)
    {
        if (instance?.TaxId is null) return null;
        var isValid = await _taxService.ValidateAsync(instance.TaxId, cancellationToken);
        if (isValid) return null;
        var errors = new ErrorCollection();
        errors.Add(propertyName, $"{displayName} is not registered.");
        return errors;
    }
}

// Register it
var validator = new ValidatorAsync<Company>(new TaxIdChecker(taxService));
var errors = await validator.ValidateObjectAsync(company, cancellationToken);

You can also implement IChecker<T> / ICheckerAsync<T> directly when you need full control over the CheckObject / CheckObjectAsync loop (e.g. bulk validation in a single round-trip).


Performance

  • Compiled lambda expressions — property getters are compiled once and reused.
  • Concurrent cachingPropertyInfo, display names, validatable property lists, and compiled getters are cached in ConcurrentDictionary.
  • Cached compiled regexMatches() compiles each pattern once and stores it in a static cache.
  • Exception-safe — exceptions in rule delegates are caught and turned into error messages without crashing the validation pipeline.

Español

Características

  • Múltiples estrategias de validación — Data Annotations, API fluida e implementaciones propias de IChecker<T>.
  • Validación asíncrona — Reglas MustAsync con soporte completo de CancellationToken en toda la cadena.
  • Reglas condicionales asyncWhenAsync / UnlessAsync para condiciones que requieren operaciones asíncronas.
  • Comparación entre propiedades — Compara una propiedad con el valor de otra usando expresiones lambda.
  • Validación jerárquica — Registra validadores hijos para grafos de objetos anidados.
  • Validación de colecciones — Valida cada elemento de una colección con rutas indexadas (Items[0].Name).
  • Validación condicionalWhen / Unless para reglas dependientes del contexto.
  • Detener al primer fallo — Opción por propiedad para parar tras el primer error.
  • Validadores reutilizables — Hereda de Validator<T> o ValidatorAsync<T> para definir clases de validación reutilizables.
  • Códigos de error — Adjunta códigos opcionales a las reglas para identificación programática.
  • Evaluación segura ante excepciones — Las excepciones dentro de las reglas se capturan y se reportan como errores de validación.
  • Alto rendimiento — Expresiones lambda compiladas, metadatos de propiedades en caché y patrones regex cacheados.
  • Integración con Pitasoft.Error — Todos los resultados se devuelven como ErrorCollection.
  • Tipado fuerte — Genéricos y expresiones lambda; sin "magic strings".

Instalación

dotnet add package Pitasoft.Validation

Requiere Pitasoft.Error.


Conceptos principales

Existen dos roles de validador principales:

Clase Descripción
Validator<T> Validador síncrono. Úsalo cuando todas las reglas son síncronas.
ValidatorAsync<T> Validador asíncrono. Prefiere los métodos *Async. Los métodos síncronos delegan a los checkers — funcionan si todos los checkers registrados soportan sync; los checkers async lanzan AsyncRuleInSyncValidationException por defecto, o se omiten con AsyncRuleBehavior.Skip.

Las reglas se definen mediante checkers:

Clase Descripción
DataAnnotationsChecker<T> Lee atributos ValidationAttribute del modelo.
RuleChecker<T> API fluida para reglas síncronas.
RuleCheckerAsync<T> API fluida para reglas síncronas y asíncronas.

Las interfaces públicas siguen un contrato de dos niveles:

Interfaz Propósito
IValidator<T> Contrato de consumidor — inyecta esta en servicios, úsala para mocking y decoradores.
IValidatorAsync<T> Contrato de consumidor async — extiende IValidator<T> con métodos *Async. Las implementaciones también pueden soportar validación síncrona a través de la API heredada, según los checkers registrados.
IValidatorBuilder<T> Contrato de construcción — extiende IValidator<T>, añade RegisterChecker. Úsala al heredar.

Primeros pasos

1. Validación con Data Annotations

La forma más sencilla de empezar es usando los atributos ya presentes en tus modelos.

using System.ComponentModel.DataAnnotations;
using Pitasoft.Validation;
using Pitasoft.Validation.DataAnnotations;

public class User
{
    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string Name { get; set; }

    [Range(18, 99)]
    public int Age { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }
}

// Usando un Validator con DataAnnotationsChecker
var validator = new Validator<User>(new DataAnnotationsChecker<User>());
var errors = validator.ValidateObject(user);

if (errors.HasErrors)
{
    foreach (var entry in errors)
        Console.WriteLine($"{entry.Key}: {string.Join(", ", entry.Value)}");
}

// O mediante método de extensión (forma más corta)
using Pitasoft.Validation.Extensions;

var errors = user.ValidateWithAttributes();
2. API fluida

Define reglas programáticamente sin modificar tus modelos.

using Pitasoft.Validation.Rules;
using Pitasoft.Validation.Extensions;

var checker = new RuleChecker<User>();

checker.For(u => u.Name)
    .StopOnFirstFailure()
    .NotNullOrEmpty("El nombre es obligatorio")
    .MinLength(2, "El nombre es demasiado corto (mín. {1} chars)")  // {1} = valor
    .MaxLength(50, "El {0} no puede superar 50 caracteres")          // {0} = nombre visible
    .Matches(@"^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$", "Solo se permiten letras");

checker.For(u => u.Age)
    .Range(18, 99, "La edad debe estar entre 18 y 99");

checker.For(u => u.Email)
    .NotNullOrEmpty("El email es obligatorio")
    .Email("Formato de email no válido");

var errors = user.ValidateWithChecker(checker);

// O inline sin crear el checker explícitamente
var errors = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Nombre obligatorio").MaxLength(50, "Demasiado largo");
    v.For(u => u.Age).Range(18, 99, "Edad inválida");
});
3. Validadores reutilizables

Hereda de Validator<T> para encapsular la lógica de validación en una clase reutilizable.

public class UserValidator : Validator<User>
{
    public UserValidator()
    {
        var checker = new RuleChecker<User>();

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("El nombre es obligatorio", code: "NOMBRE_REQUERIDO")
            .MinLength(2, "El nombre es demasiado corto", code: "NOMBRE_MIN")
            .MaxLength(50, "El nombre es demasiado largo", code: "NOMBRE_MAX");

        checker.For(u => u.Age)
            .Range(18, 99, "La edad debe estar entre 18 y 99", code: "EDAD_INVALIDA");

        RegisterChecker(checker);
    }
}

// Uso
var validator = new UserValidator();
var errors = validator.ValidateObject(user);

bool esValido = validator.IsValid(user);

if (!validator.TryValidate(user, out var errors))
{
    // manejar errores
}
4. Validación asíncrona

Usa ValidatorAsync<T> cuando alguna regla requiere operaciones asíncronas (p.ej. consultas a BD). Prefiere las variantes *Async. Los métodos síncronos siguen delegando en los checkers registrados, por lo que la validación sync puede funcionar completamente, validar de forma parcial o lanzar AsyncRuleInSyncValidationException según las capacidades sync de esos checkers y su AsyncRuleBehavior.

public class UserValidator : ValidatorAsync<User>
{
    public UserValidator(IUserRepository repo)
    {
        var checker = new RuleCheckerAsync<User>();

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("El nombre es obligatorio")
            .MaxLength(50, "El nombre es demasiado largo")
            // Regla async con CancellationToken
            .MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name, ct),
                "El nombre ya está en uso", code: "NOMBRE_DUPLICADO");

        checker.For(u => u.Email)
            .NotNullOrEmpty("El email es obligatorio")
            .Email("Formato de email no válido")
            .MustAsync(async u => !await repo.EmailExistsAsync(u.Email),
                "El email ya está registrado");

        RegisterChecker(checker);
    }
}

// Uso — prefiere siempre la ruta async
var validator = new UserValidator(repo);
var errors = await validator.ValidateObjectAsync(user);

// Con CancellationToken
var errors = await validator.ValidateObjectAsync(user, cancellationToken);

// Métodos helper
bool esValido = await validator.IsValidAsync(user, cancellationToken);
var (esValido, errors) = await validator.TryValidateAsync(user, cancellationToken);

Validación síncrona en ValidatorAsync<T>: Los métodos síncronos (ValidateObject, ValidateProperty, …) están disponibles y delegan a cada checker registrado. Según esos checkers, la validación sync puede completarse por entero, realizar validación parcial síncrona o lanzar AsyncRuleInSyncValidationException. AsyncRuleBehavior.Skip es el mecanismo que permite esa validación parcial en checkers de reglas async.

var checker = new RuleCheckerAsync<User>();
checker.For(u => u.Name)
    .NotNullOrEmpty("El nombre es obligatorio")          // regla síncrona
    .MustAsync(async u => ..., "El nombre ya existe");   // regla asíncrona

// Omitir reglas async en validación síncrona (p.ej. feedback en tiempo real en UI)
checker.AsyncRuleBehavior = AsyncRuleBehavior.Skip;
var errors = validator.ValidateObject(user); // ejecuta solo las reglas síncronas
5. Validar propiedades específicas

Valida un subconjunto de propiedades; útil para validación en tiempo real de formularios.

// Síncrono
var errors = validator.ValidateProperties(user, nameof(User.Name), nameof(User.Email));
var errors = validator.ValidateProperty(user, nameof(User.Name));

// Asíncrono
var errors = await validator.ValidatePropertiesAsync(user, nameof(User.Name));
var errors = await validator.ValidatePropertyAsync(user, nameof(User.Email), cancellationToken);

// Con expresión lambda (método de extensión)
var errors = validator.ValidateProperty(user, u => u.Name);
var errors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Referencia de la API fluida

Reglas de cadenas de texto
Método Descripción
NotNullOrEmpty(msg) El valor no es nulo, vacío ni espacios en blanco
MinLength(min, msg) Longitud de cadena ≥ min
MaxLength(max, msg) Longitud de cadena ≤ max
Length(exact, msg) Longitud exacta
LengthBetween(min, max, msg) Longitud dentro del rango
Matches(pattern, msg) Coincide con expresión regular (patrón cacheado)
Email(msg) Formato de email válido
Reglas de nulidad e igualdad
Método Descripción
NotNull(msg) El valor no es nulo
Null(msg) El valor es nulo
Equal(valor, msg) El valor es igual al literal indicado
Equal(x => x.Otra, msg) El valor es igual al de otra propiedad
NotEqual(valor, msg) El valor es distinto al literal indicado
NotEqual(x => x.Otra, msg) El valor es distinto al de otra propiedad
Reglas de comparación y rango
Método Descripción
Range(min, max, msg) El valor está dentro del rango (inclusive)
GreaterThan(min, msg) Valor > literal
GreaterThan(x => x.Otra, msg) Valor > otra propiedad
GreaterThanOrEqual(min, msg) Valor ≥ literal
GreaterThanOrEqual(x => x.Otra, msg) Valor ≥ otra propiedad
LessThan(max, msg) Valor < literal
LessThan(x => x.Otra, msg) Valor < otra propiedad
LessThanOrEqual(max, msg) Valor ≤ literal
LessThanOrEqual(x => x.Otra, msg) Valor ≤ otra propiedad
Reglas de colecciones
Método Descripción
NotEmpty(msg) La colección no es nula y tiene al menos un elemento
MinItems(min, msg) La colección tiene al menos min elementos
MaxItems(max, msg) La colección tiene como máximo max elementos
Reglas personalizadas y condicionales
Método Descripción
Must(predicate, msg) Regla síncrona personalizada; recibe la instancia completa
MustAsync(predicate, msg) Regla async personalizada (Func<T, Task<bool>>)
MustAsync((t, ct) => ..., msg) Regla async personalizada con CancellationToken
When(condition, configure) Aplica reglas solo cuando la condición síncrona es true
Unless(condition, configure) Aplica reglas solo cuando la condición síncrona es false
WhenAsync(condition, configure) Aplica reglas solo cuando la condición async es true
UnlessAsync(condition, configure) Aplica reglas solo cuando la condición async es false
StopOnFirstFailure() Para tras la primera regla fallida de esta propiedad

Todos los métodos aceptan un parámetro code opcional:

checker.For(u => u.Name)
    .NotNullOrEmpty("El nombre es obligatorio", code: "NOMBRE_REQUERIDO");
Marcadores de posición en mensajes de error
Marcador Valor
{0} Nombre visible (desde [Display(Name = "...")] o el nombre de la propiedad)
{1} Valor actual de la propiedad

Uso avanzado

Validación condicional (When / Unless)
var checker = new RuleChecker<User>();

// La regla se aplica solo cuando la condición es verdadera
checker.For(u => u.NombreEmpresa)
    .When(u => u.EsEmpresa,
        b => b.NotNullOrEmpty("El nombre de empresa es obligatorio para empresas"));

// La regla se aplica solo cuando la condición es falsa
checker.For(u => u.Dni)
    .Unless(u => u.EsEmpresa,
        b => b.NotNullOrEmpty("El DNI es obligatorio para personas físicas"));
Validación condicional asíncrona (WhenAsync / UnlessAsync)

Usa WhenAsync en un RuleCheckerAsync<T> cuando la propia condición requiere una operación asíncrona.

var checker = new RuleCheckerAsync<User>();

checker.For(u => u.CodigoDescuento)
    .WhenAsync(async u => await featureService.IsEnabledAsync("descuentos"),
        b => b.MustAsync(async u => await descuentoRepo.IsValidAsync(u.CodigoDescuento),
            "El código de descuento no es válido"));
Comparación entre propiedades

Compara el valor de una propiedad con el de otra propiedad del mismo objeto.

var checker = new RuleChecker<Pedido>();

// FechaFin debe ser posterior a FechaInicio
checker.For(o => o.FechaFin)
    .GreaterThan(o => o.FechaInicio, "La fecha de fin debe ser posterior a la de inicio");

// ConfirmarContraseña debe coincidir con Contraseña
checker.For(u => u.ConfirmarContrasena)
    .Equal(u => u.Contrasena, "Las contraseñas no coinciden");

// MaxArticulos debe ser >= MinArticulos
checker.For(o => o.MaxArticulos)
    .GreaterThanOrEqual(o => o.MinArticulos, "El máximo debe ser ≥ al mínimo");
Reglas cruzadas entre propiedades con Must

Must recibe la instancia completa del objeto, facilitando validaciones que dependen de múltiples campos:

checker.For(u => u.AnosExperiencia)
    .Must(u => u.AnosExperiencia >= u.ExperienciaMinRequerida,
        "Los años de experiencia ({1}) deben ser al menos la experiencia mínima requerida");
Detener al primer fallo

Para la evaluación de reglas de una propiedad tras el primer fallo, evitando mensajes de error redundantes:

checker.For(u => u.Name)
    .StopOnFirstFailure()          // solo se reporta el primer error
    .NotNullOrEmpty("El nombre es obligatorio")
    .MinLength(2, "El nombre es demasiado corto")
    .MaxLength(50, "El nombre es demasiado largo");
Validadores hijos (objetos anidados)

Registra un validador para una propiedad anidada. Los errores se prefijan con la ruta de la propiedad.

var addressValidator = new Validator<Address>(new DataAnnotationsChecker<Address>());

var orderValidator = new Validator<Order>();
orderValidator.RegisterChildValidator(o => o.DireccionEnvio, addressValidator);

var errors = orderValidator.ValidateObject(order);
// Claves de error: "DireccionEnvio.Ciudad", "DireccionEnvio.CodigoPostal", etc.

Los validadores hijos también pueden ser asíncronos:

var addressValidator = new ValidatorAsync<Address>();
// configurar addressValidator...

var orderValidator = new ValidatorAsync<Order>();
orderValidator.RegisterChildValidator(o => o.DireccionEnvio, addressValidator);

var errors = await orderValidator.ValidateObjectAsync(order, cancellationToken);
Validación de colecciones (ForEach)

Valida cada elemento de una colección individualmente. Los errores incluyen el índice del elemento.

var validator = new Validator<Pedido>();
validator.ForEach(o => o.Lineas, checker =>
{
    checker.For(l => l.Cantidad).GreaterThan(0, "La cantidad debe ser mayor que cero");
    checker.For(l => l.ProductoId).NotNullOrEmpty("El producto es obligatorio");
});

var errors = validator.ValidateObject(pedido);
// Claves de error: "Lineas[0].Cantidad", "Lineas[1].ProductoId", etc.

Con reglas asíncronas:

var validator = new ValidatorAsync<Pedido>();
validator.ForEach(o => o.Lineas, (RuleCheckerAsync<LineaPedido> checker) =>
{
    checker.For(l => l.ProductoId)
        .NotNullOrEmpty("El producto es obligatorio")
        .MustAsync(async (l, ct) => await productoRepo.ExistsAsync(l.ProductoId, ct),
            "El producto no existe");
});

var errors = await validator.ValidateObjectAsync(pedido, cancellationToken);
Combinación de DataAnnotations y reglas fluidas

Registra varios checkers en el mismo validador:

var ruleChecker = new RuleChecker<User>();
ruleChecker.For(u => u.Name)
    .Must(u => !u.Name.Contains("Admin", StringComparison.OrdinalIgnoreCase),
        "El nombre no puede contener 'Admin'");

// DataAnnotationsChecker valida [Required], [StringLength], etc.
// RuleChecker añade la regla personalizada adicional.
var validator = new Validator<User>(new DataAnnotationsChecker<User>(), ruleChecker);
var errors = validator.ValidateObject(user);

Métodos de extensión

Extensiones sobre objetos (ObjectExtensions)

Valida directamente un objeto sin crear una instancia de validador:

using Pitasoft.Validation.Extensions;

// Data Annotations
var errors = user.ValidateWithAttributes();
bool esValido = user.TryValidateWithAttributes(out var errors);

// Checker síncrono
var errors = user.ValidateWithChecker(checker);
bool esValido = user.TryValidateWithChecker(checker, out var errors);

// Reglas síncronas inline
var errors = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Obligatorio");
    v.For(u => u.Age).Range(18, 99, "Edad inválida");
});

// Checker asíncrono (con CancellationToken)
var errors = await user.ValidateWithCheckerAsync(asyncChecker, cancellationToken);

// Reglas asíncronas inline (con CancellationToken)
var errors = await user.ValidateWithValidatorAsync(v =>
{
    v.For(u => u.Email).MustAsync(async u => !await repo.EmailExistsAsync(u.Email), "Email en uso");
}, cancellationToken);
Extensiones de validador (ValidatorExtensions)
using Pitasoft.Validation.Extensions;

// Síncrono
bool esValido = validator.IsValid(user);
bool esValido = validator.TryValidate(user, out var errors);

// Asíncrono (IValidatorAsync<T>)
bool esValido = await validator.IsValidAsync(user, cancellationToken);
var (esValido, errors) = await validator.TryValidateAsync(user, cancellationToken);

// Validar una propiedad específica con lambda
var errors = validator.ValidateProperty(user, u => u.Name);
var errors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Checkers personalizados

La forma recomendada de crear checkers personalizados es heredar de las clases base abstractas. Estas proporcionan el bucle CheckObject / CheckObjectAsync por defecto, de modo que solo necesitas implementar la lógica de validación por propiedad.

Checker síncrono — CheckerBase<T>

Implementa GetProperties y Check. CheckObject se deriva automáticamente.

public class NifFormatoChecker : CheckerBase<Empresa>
{
    public override IEnumerable<string> GetProperties() => [nameof(Empresa.Nif)];

    public override ErrorCollection? Check(Empresa? instance, string propertyName, string displayName)
    {
        if (instance?.Nif is null) return null;
        if (TieneFormatoValido(instance.Nif)) return null;
        var errors = new ErrorCollection();
        errors.Add(propertyName, $"El formato del {displayName} no es válido.");
        return errors;
    }

    private static bool TieneFormatoValido(string nif) => nif.Length == 9;
}

// Registro
var validator = new Validator<Empresa>(new NifFormatoChecker());
var errors = validator.ValidateObject(empresa);
Checker asíncrono — CheckerAsyncBase<T>

Implementa GetProperties y CheckAsync. CheckObjectAsync se deriva automáticamente. Las entradas síncronas (Check / CheckObject) lanzan una excepción porque un checker async debe usarse por la ruta de validación asíncrona.

public class NifChecker : CheckerAsyncBase<Empresa>
{
    private readonly IAeatService _aeatService;

    public NifChecker(IAeatService aeatService) => _aeatService = aeatService;

    public override IEnumerable<string> GetProperties() => [nameof(Empresa.Nif)];

    public override async Task<ErrorCollection?> CheckAsync(Empresa? instance, string propertyName,
        string displayName, CancellationToken cancellationToken = default)
    {
        if (instance?.Nif is null) return null;
        var esValido = await _aeatService.ValidarAsync(instance.Nif, cancellationToken);
        if (esValido) return null;
        var errors = new ErrorCollection();
        errors.Add(propertyName, $"El {displayName} no está registrado en la AEAT.");
        return errors;
    }
}

// Registro
var validator = new ValidatorAsync<Empresa>(new NifChecker(aeatService));
var errors = await validator.ValidateObjectAsync(empresa, cancellationToken);

También puedes implementar IChecker<T> / ICheckerAsync<T> directamente cuando necesites control total sobre el bucle CheckObject / CheckObjectAsync (p.ej. validación en bloque en un único round-trip).


Rendimiento

  • Expresiones lambda compiladas — los getters de propiedades se compilan una vez y se reutilizan.
  • Caché concurrentePropertyInfo, nombres visibles, listas de propiedades validables y getters compilados se almacenan en ConcurrentDictionary.
  • Regex compilados y cacheadosMatches() compila cada patrón una vez y lo almacena en un caché estático.
  • Seguro ante excepciones — las excepciones en los delegados de reglas se capturan y convierten en mensajes de error sin interrumpir el pipeline de validación.

Autor

Sebastián Martínez Pérez

Licencia

Copyright © 2020-2026 Pitasoft, S.L. Distribuido bajo la licencia incluida en LICENSE.txt.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (4)

Showing the top 4 NuGet packages that depend on Pitasoft.Validation:

Package Downloads
Pitasoft.Validation.Rules

Implement rules to validate objects

Pitasoft.FluentValidation

Provides integration with FluentValidation for Pitasoft.Validation

Pitasoft.Validation.AspNetCore

ASP.NET Core integration for Pitasoft.Validation. Provides Minimal API endpoint filters and MVC action filters to validate request parameters using IValidator<T>, IValidatorAsync<T>, IChecker<T>, and ICheckerAsync<T>.

Pitasoft.Validation.DependencyInjectionExtensions

Microsoft.Extensions.DependencyInjection integration for Pitasoft.Validation. Provides extension methods to register IValidator<T> and IValidatorAsync<T> implementations into an IServiceCollection, both manually and via assembly scanning.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.0.5 253 4/1/2026
6.0.4 197 3/31/2026
6.0.3 126 3/25/2026
6.0.2 120 3/25/2026
6.0.1 128 3/23/2026
5.1.5 129 3/17/2026
5.1.4 129 3/17/2026
5.1.3 156 3/5/2026
5.1.2 140 3/2/2026
5.1.1 132 3/2/2026
5.0.1 132 2/25/2026
4.3.0 502 11/20/2023
4.2.0 315 10/23/2023
4.1.0 774 11/18/2022
4.0.2 985 7/26/2022
4.0.0 841 7/26/2022
3.0.0 640 7/22/2022
2.0.0 682 6/26/2021
0.0.1 132 3/23/2026