Pitasoft.Validation 6.0.5

dotnet add package Pitasoft.Validation --version 6.0.5
                    
NuGet\Install-Package Pitasoft.Validation -Version 6.0.5
                    
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="6.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pitasoft.Validation" Version="6.0.5" />
                    
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 6.0.5
                    
#r "nuget: Pitasoft.Validation, 6.0.5"
                    
#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@6.0.5
                    
#: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=6.0.5
                    
Install as a Cake Addin
#tool nuget:?package=Pitasoft.Validation&version=6.0.5
                    
Install as a Cake Tool

Pitasoft.Validation

Build Status Code Coverage NuGet Version NuGet Downloads Pitasoft.Error Version xUnit Nullable License: MIT .NET Versions

Pitasoft.Validation is a flexible and high-performance .NET validation library built on top of Pitasoft.Error. It supports Data Annotations, fluent rules, custom checkers, nested validators, collection validation, and mixed sync/async validation flows.


English

Overview

The library is organized around two ideas:

  • Checkers define how validation is performed.
  • Validators orchestrate one or more checkers and expose the consumer-facing API.

Validation results are returned as ErrorCollection, so Pitasoft.Error semantics are part of the public contract:

  • property-level errors use the property name as key
  • object-level errors use string.Empty
  • nested validation rewrites keys with prefixes such as Address.City
  • collection validation rewrites keys such as Items[0].Name

Features

  • Data Annotations validation through DataAnnotationsChecker<T>
  • Fluent synchronous rules with RuleChecker<T>
  • Fluent synchronous and asynchronous rules with RuleCheckerAsync<T>
  • Custom IChecker<T> and ICheckerAsync<T> implementations
  • Child validators for nested object graphs
  • Collection validation with indexed error paths
  • Conditional rules with When / Unless
  • Async conditional rules with WhenAsync / UnlessAsync
  • Per-property StopOnFirstFailure
  • Error codes on rules
  • Partial sync validation support through AsyncRuleBehavior.Skip
  • Direct object extension methods for quick validation scenarios
  • Nullable-aware modern C# API

Installation

dotnet add package Pitasoft.Validation

Pitasoft.Error is required and installed transitively by the package.

Supported frameworks

  • .NET 8
  • .NET 9
  • .NET 10

Public building blocks

Validators
Type Role
Validator<T> Main synchronous validator
ValidatorAsync<T> Main asynchronous validator
IValidator<T> Consumer contract for sync validation
IValidatorAsync<T> Consumer contract for async validation
IValidatorBuilder<T> Construction contract for registering checkers
Checkers
Type Role
IChecker<T> Base sync checker contract
ICheckerAsync<T> Async checker contract
CheckerBase<T> Base class for sync checkers
CheckerAsyncBase<T> Base class for strict async-only checkers
DataAnnotationsChecker<T> Validation from ValidationAttribute metadata
RuleChecker<T> Fluent sync rule checker
RuleCheckerAsync<T> Fluent sync+async rule checker
Rule and async behavior types
Type Role
RuleBase<T> Base rule abstraction
DelegateRule<T> Sync delegate-based rule
DelegateRuleAsync<T> Async delegate-based rule
IAsyncRuleBehavior Contract for configurable sync behavior when async rules exist
AsyncRuleBehavior Throw or Skip
AsyncRuleInSyncValidationException Thrown when sync validation reaches async-only behavior
AsyncRuleRegistrationException Thrown when registering an async rule in RuleChecker<T>

How the model works

IChecker<T> and ICheckerAsync<T>

A checker is a validation strategy.

IChecker<T> exposes:

  • GetProperties()
  • Check(instance, propertyName, displayName)
  • CheckObject(instance)

ICheckerAsync<T> extends that model with:

  • CheckAsync(instance, propertyName, displayName, cancellationToken)
  • CheckObjectAsync(instance, cancellationToken)

Use a checker when you want a reusable validation component or a custom integration point.

IValidator<T> and IValidatorAsync<T>

A validator is the consumer-facing facade.

IValidator<T> exposes:

  • ValidateObject
  • ValidateProperties
  • ValidateProperty

IValidatorAsync<T> adds:

  • ValidateObjectAsync
  • ValidatePropertiesAsync
  • ValidatePropertyAsync

Use a validator when the rest of your application needs “the validator for type T”.

Sync and async semantics

This library uses a flexible, explicit sync/async model:

  • Validator<T> is the synchronous validator.
  • ValidatorAsync<T> is the asynchronous validator.
  • RuleChecker<T> only accepts synchronous rules.
  • RuleCheckerAsync<T> accepts both synchronous and asynchronous rules.
  • CheckerAsyncBase<T> is a strict async-only base class: sync entry points always throw.

When synchronous validation reaches async-capable behavior, AsyncRuleBehavior controls what happens:

  • Throw
    throws AsyncRuleInSyncValidationException
  • Skip
    skips async-only rules and performs partial synchronous validation

That means:

  • async validators can run sync and async checkers
  • sync validators remain sync-first
  • partial sync validation is intentional, never silent magic

Project contents

The Pitasoft.Validation project is organized into these areas:

Folder / file area Purpose
DataAnnotations/ Data Annotations integration
Rules/ Rule abstractions and rule checkers
Helpers/ Child validators, collection validators, builders, caches
Extensions/ Convenience APIs for validators, objects, and rule checkers
Validator{T}.cs / ValidatorAsync{T}.cs Main validator implementations
IChecker*.cs / IValidator*.cs Public contracts
Excepcions/ Public exceptions related to async registration/execution

Typical execution flow

This is the usual runtime flow:

  1. Your application calls a validator such as Validator<User> or ValidatorAsync<User>.
  2. The validator resolves the registered checkers for the requested object or property.
  3. Each checker evaluates its own rules or delegated validation strategy.
  4. All errors are merged into a single ErrorCollection.
  5. Child validators and collection validators rewrite keys to preserve the object path.

Conceptually:

  • Validator = orchestration facade
  • Checker = validation strategy
  • Rule = smallest executable validation unit

Getting started

1. Data Annotations
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; }
}

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

You can also register Data Annotations on an existing validator:

using Pitasoft.Validation.Extensions;

var validator = new Validator<User>()
    .RegisterDataAnnotationsChecker();

Or validate directly from the object:

using Pitasoft.Validation.Extensions;

var errors = user.ValidateWithAttributes();
2. Fluent synchronous rules
using Pitasoft.Validation.Rules;

var checker = new RuleChecker<User>();

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

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 validator = new Validator<User>(checker);
var errors = validator.ValidateObject(user);
3. Reusable validator classes
using Pitasoft.Validation.Rules;

public sealed 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")
            .MaxLength(50, "Name is too long", code: "NAME_MAX");

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

        RegisterChecker(checker);
    }
}

Useful validator extensions:

using Pitasoft.Validation.Extensions;

var validator = new UserValidator();

var errors = validator.ValidateObject(user);
bool isValid = validator.IsValid(user);
bool ok = validator.TryValidate(user, out var richErrors);
4. Fluent asynchronous rules
using Pitasoft.Validation.Rules;

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

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("Name is required")
            .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");

        RegisterChecker(checker);
    }
}

Usage:

using Pitasoft.Validation.Extensions;

var validator = new UserValidator(repo);

var errors = await validator.ValidateObjectAsync(user, cancellationToken);
bool isValid = await validator.IsValidAsync(user, cancellationToken);
var (ok, richErrors) = await validator.TryValidateAsync(user, cancellationToken);
5. Conditional and async conditional rules
checker.For(u => u.Status)
    .NotNullOrEmpty("Status is required")
    .Unless(u => string.IsNullOrWhiteSpace(u.Status), b =>
        b.Must(u => u.Status is "Active" or "Pending" or "Inactive",
            "Status is invalid"));

checker.For(u => u.Name)
    .WhenAsync(async (u, ct) => await repo.MustCheckNameAsync(u.Id, ct), b =>
        b.MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name!, ct),
            "Name is already taken"));
6. Child validators
using Pitasoft.Validation.Rules;

var addressChecker = new RuleCheckerAsync<Address>();
addressChecker.For(a => a.City).NotNullOrEmpty("City is required");
addressChecker.For(a => a.Street).NotNullOrEmpty("Street is required");

var addressValidator = new ValidatorAsync<Address>(addressChecker);

var userValidator = new ValidatorAsync<User>();
userValidator.RegisterChildValidator(u => u.Address!, addressValidator);

Errors are rewritten with the property prefix:

  • City becomes Address.City
  • object-level child errors become Address
7. Collection validation with ForEach
var validator = new ValidatorAsync<User>();

validator.ForEach(u => u.Emails, (RuleChecker<string> itemChecker) =>
{
    itemChecker.For(e => e.Length)
        .GreaterThan(5, "Email item is too short");
});

Async item validation:

validator.ForEach(u => u.Orders, (RuleCheckerAsync<Order> itemChecker) =>
{
    itemChecker.For(o => o.Number)
        .NotNullOrEmpty("Order number is required")
        .MustAsync(async (o, ct) => !await repo.OrderExistsAsync(o.Number!, ct),
            "Order number already exists");
});

Errors are rewritten as indexed paths such as:

  • Orders[0].Number
  • Emails[1].Length
8. Custom async checkers

Use CheckerAsyncBase<T> when you want a strict async-only checker:

using Pitasoft.Error;

public sealed class UniqueEmailChecker : CheckerAsyncBase<User>
{
    public override IEnumerable<string> GetProperties() => [nameof(User.Emails)];

    public override async Task<ErrorCollection?> CheckAsync(
        User? instance,
        string propertyName,
        string displayName,
        CancellationToken cancellationToken = default)
    {
        if (instance is null) return null;

        await Task.Delay(10, cancellationToken);

        ErrorCollection? errors = null;
        foreach (var email in instance.Emails)
        {
            if (email == "taken@example.com")
            {
                (errors ??= new ErrorCollection()).Add(propertyName,
                    $"Email '{email}' is already registered");
            }
        }

        return errors is { HasErrors: true } ? errors : null;
    }
}
9. Object-level extension methods

For quick use without explicitly creating validator types:

using Pitasoft.Validation.Extensions;

var errors1 = user.ValidateWithAttributes();
var errors2 = user.ValidateWithChecker(checker);

var errors3 = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Name is required");
    v.For(u => u.Age).Range(18, 99, "Invalid age");
});

var errors4 = await user.ValidateWithValidatorAsync(v =>
{
    v.For(u => u.Name)
        .NotNullOrEmpty("Name is required")
        .MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name!, ct),
            "Duplicated name");
}, cancellationToken);
10. Validating specific properties

Property-level validation is useful for UI forms, step-by-step workflows, or incremental validation.

var validator = new UserValidator(repo);

// Sync
var nameErrors = validator.ValidateProperty(user, nameof(User.Name));
var subsetErrors = validator.ValidateProperties(user, nameof(User.Name), nameof(User.Email));

// Async
var asyncNameErrors = await validator.ValidatePropertyAsync(user, nameof(User.Name), cancellationToken);
var asyncSubsetErrors = await validator.ValidatePropertiesAsync(
    user,
    new[] { nameof(User.Name), nameof(User.Email) },
    cancellationToken);

With lambda expressions:

using Pitasoft.Validation.Extensions;

var syncErrors = validator.ValidateProperty(user, u => u.Name);
var asyncErrors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Fluent rule catalog

The fluent builder currently includes these main categories:

  • nullability: NotNull, Null, NotNullOrEmpty
  • string rules: MinLength, MaxLength, Length, LengthBetween, Matches, Email, StartsWith, EndsWith, Url
  • equality and sets: Equal, NotEqual, In, NotIn
  • comparisons: Range, ExclusiveBetween, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
  • collections: NotEmpty, MinItems, MaxItems
  • composition: When, Unless, WhenAsync, UnlessAsync, StopOnFirstFailure
  • custom delegates: Must, MustAsync

Error formatting

Error messages support placeholders:

  • {0} = display name
  • {1} = current property value

This is handled when rule failures are added to ErrorCollection.

Error key examples

Typical keys returned by the library:

Scenario Example key
Property validation Name
Object-level validation string.Empty
Child validator property Address.City
Child validator object error Address
Collection item property Orders[0].Number
Collection item object error Orders[0].

Behavior notes

  • RuleChecker<T> rejects async rule registration and throws AsyncRuleRegistrationException
  • RuleCheckerAsync<T> can run both sync and async rules
  • sync calls on async-capable flows may throw AsyncRuleInSyncValidationException
  • AsyncRuleBehavior.Skip enables partial sync validation when that behavior is intentional
  • CheckerAsyncBase<T> is stricter than RuleCheckerAsync<T>: sync entry points always throw

When to use what

  • Use DataAnnotationsChecker<T> when your model already has attributes
  • Use RuleChecker<T> for synchronous business rules
  • Use RuleCheckerAsync<T> when any rule needs async I/O
  • Use Validator<T> / ValidatorAsync<T> for application-facing validation services
  • Use CheckerAsyncBase<T> for custom async domain validators that should never run synchronously

Practical recommendations

  • Prefer ValidatorAsync<T> whenever the validation pipeline might need I/O.
  • Use AsyncRuleBehavior.Skip only for intentional partial sync validation scenarios such as UI pre-validation.
  • Keep child validators focused on a single aggregate or nested type.
  • Use ForEach for item-level collection validation instead of manual loops in application code.
  • Treat ErrorCollection shape as public behavior if other layers depend on keys.

Español

Resumen

La librería se apoya en dos conceptos:

  • Checkers: definen cómo se valida
  • Validators: orquestan uno o varios checkers y exponen la API de consumo

Todos los resultados se devuelven como ErrorCollection, así que la semántica de Pitasoft.Error forma parte del contrato público:

  • los errores de propiedad usan el nombre de la propiedad
  • los errores de objeto usan string.Empty
  • la validación anidada reescribe claves como Address.City
  • las colecciones reescriben claves como Items[0].Name

Características

  • Validación mediante Data Annotations con DataAnnotationsChecker<T>
  • Reglas fluentes síncronas con RuleChecker<T>
  • Reglas fluentes síncronas y asíncronas con RuleCheckerAsync<T>
  • Checkers personalizados con IChecker<T> e ICheckerAsync<T>
  • Validación de hijos con RegisterChildValidator
  • Validación de colecciones con ForEach
  • Reglas condicionales When / Unless
  • Reglas condicionales asíncronas WhenAsync / UnlessAsync
  • StopOnFirstFailure por propiedad
  • Códigos de error opcionales
  • Validación parcial sync mediante AsyncRuleBehavior.Skip
  • Métodos de extensión para validar directamente objetos

Instalación

dotnet add package Pitasoft.Validation

Pitasoft.Error es una dependencia necesaria y llega de forma transitiva.

Frameworks soportados

  • .NET 8
  • .NET 9
  • .NET 10

Elementos públicos principales

Validators
Tipo Función
Validator<T> Validador síncrono principal
ValidatorAsync<T> Validador asíncrono principal
IValidator<T> Contrato de consumo sync
IValidatorAsync<T> Contrato de consumo async
IValidatorBuilder<T> Contrato de construcción para registrar checkers
Checkers
Tipo Función
IChecker<T> Contrato base de checker sync
ICheckerAsync<T> Contrato base de checker async
CheckerBase<T> Clase base para checkers síncronos
CheckerAsyncBase<T> Clase base para checkers async estrictos
DataAnnotationsChecker<T> Adaptador de ValidationAttribute
RuleChecker<T> Checker fluente sync
RuleCheckerAsync<T> Checker fluente sync + async
Tipos de reglas y comportamiento async
Tipo Función
RuleBase<T> Abstracción base de regla
DelegateRule<T> Regla sync basada en delegado
DelegateRuleAsync<T> Regla async basada en delegado
IAsyncRuleBehavior Contrato para controlar la validación sync cuando existen reglas async
AsyncRuleBehavior Throw o Skip
AsyncRuleInSyncValidationException Excepción al alcanzar comportamiento async desde una ruta sync
AsyncRuleRegistrationException Excepción al intentar registrar una regla async en RuleChecker<T>

Cómo funciona el modelo

IChecker<T> e ICheckerAsync<T>

Un checker es una estrategia de validación.

IChecker<T> expone:

  • GetProperties()
  • Check(instance, propertyName, displayName)
  • CheckObject(instance)

ICheckerAsync<T> añade:

  • CheckAsync(instance, propertyName, displayName, cancellationToken)
  • CheckObjectAsync(instance, cancellationToken)

Úsalo cuando quieras una pieza reutilizable de validación o una integración personalizada.

IValidator<T> e IValidatorAsync<T>

Un validator es la fachada orientada al consumidor.

IValidator<T> expone:

  • ValidateObject
  • ValidateProperties
  • ValidateProperty

IValidatorAsync<T> añade:

  • ValidateObjectAsync
  • ValidatePropertiesAsync
  • ValidatePropertyAsync

Úsalo cuando tu aplicación necesite “el validador de T”.

Semántica sync/async

El modelo sync/async de la librería es explícito:

  • Validator<T> es el validador síncrono
  • ValidatorAsync<T> es el validador asíncrono
  • RuleChecker<T> solo acepta reglas síncronas
  • RuleCheckerAsync<T> acepta reglas síncronas y asíncronas
  • CheckerAsyncBase<T> es estrictamente async: las entradas sync siempre lanzan excepción

Cuando una validación síncrona encuentra comportamiento async, AsyncRuleBehavior decide:

  • Throw
    lanza AsyncRuleInSyncValidationException
  • Skip
    omite reglas async y hace validación sync parcial

Esto implica que:

  • un validador async puede ejecutar checkers sync y async
  • un flujo sync sigue siendo sync-first
  • la validación parcial sync es intencional y configurable

Contenido del proyecto

El proyecto Pitasoft.Validation se organiza así:

Carpeta / área Propósito
DataAnnotations/ Integración con Data Annotations
Rules/ Reglas, delegados y rule checkers
Helpers/ Builders, validadores de hijos, colecciones y cachés
Extensions/ Métodos de extensión de conveniencia
Validator{T}.cs / ValidatorAsync{T}.cs Implementaciones principales
IChecker*.cs / IValidator*.cs Contratos públicos
Excepcions/ Excepciones públicas relacionadas con reglas async

Flujo típico de ejecución

El flujo habitual en tiempo de ejecución es:

  1. La aplicación invoca un validator como Validator<User> o ValidatorAsync<User>.
  2. El validator resuelve los checkers registrados para el objeto o la propiedad.
  3. Cada checker evalúa sus reglas o su estrategia de validación.
  4. Todos los errores se agregan en un único ErrorCollection.
  5. Los validadores hijos y de colecciones reescriben las claves para conservar la ruta completa.

Conceptualmente:

  • Validator = fachada de orquestación
  • Checker = estrategia de validación
  • Rule = unidad mínima ejecutable

Primeros pasos

1. Data Annotations
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; }
}

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

También puedes registrar Data Annotations sobre un validator existente:

using Pitasoft.Validation.Extensions;

var validator = new Validator<User>()
    .RegisterDataAnnotationsChecker();

O validar directamente desde el objeto:

using Pitasoft.Validation.Extensions;

var errors = user.ValidateWithAttributes();
2. Reglas fluentes síncronas
using Pitasoft.Validation.Rules;

var checker = new RuleChecker<User>();

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

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 validator = new Validator<User>(checker);
var errors = validator.ValidateObject(user);
3. Validators reutilizables
using Pitasoft.Validation.Rules;

public sealed 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")
            .MaxLength(50, "Name is too long", code: "NAME_MAX");

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

        RegisterChecker(checker);
    }
}

Extensiones útiles del validator:

using Pitasoft.Validation.Extensions;

var validator = new UserValidator();

var errors = validator.ValidateObject(user);
bool isValid = validator.IsValid(user);
bool ok = validator.TryValidate(user, out var richErrors);
4. Reglas fluentes asíncronas
using Pitasoft.Validation.Rules;

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

        checker.For(u => u.Name)
            .StopOnFirstFailure()
            .NotNullOrEmpty("Name is required")
            .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");

        RegisterChecker(checker);
    }
}

Uso:

using Pitasoft.Validation.Extensions;

var validator = new UserValidator(repo);

var errors = await validator.ValidateObjectAsync(user, cancellationToken);
bool isValid = await validator.IsValidAsync(user, cancellationToken);
var (ok, richErrors) = await validator.TryValidateAsync(user, cancellationToken);
5. Reglas condicionales y condiciones async
checker.For(u => u.Status)
    .NotNullOrEmpty("Status is required")
    .Unless(u => string.IsNullOrWhiteSpace(u.Status), b =>
        b.Must(u => u.Status is "Active" or "Pending" or "Inactive",
            "Status is invalid"));

checker.For(u => u.Name)
    .WhenAsync(async (u, ct) => await repo.MustCheckNameAsync(u.Id, ct), b =>
        b.MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name!, ct),
            "Name is already taken"));
6. Validadores hijos
using Pitasoft.Validation.Rules;

var addressChecker = new RuleCheckerAsync<Address>();
addressChecker.For(a => a.City).NotNullOrEmpty("City is required");
addressChecker.For(a => a.Street).NotNullOrEmpty("Street is required");

var addressValidator = new ValidatorAsync<Address>(addressChecker);

var userValidator = new ValidatorAsync<User>();
userValidator.RegisterChildValidator(u => u.Address!, addressValidator);

Las claves se reescriben con prefijo:

  • City pasa a Address.City
  • los errores de objeto del hijo pasan a Address
7. Validación de colecciones con ForEach
var validator = new ValidatorAsync<User>();

validator.ForEach(u => u.Emails, (RuleChecker<string> itemChecker) =>
{
    itemChecker.For(e => e.Length)
        .GreaterThan(5, "Email item is too short");
});

Validación async de items:

validator.ForEach(u => u.Orders, (RuleCheckerAsync<Order> itemChecker) =>
{
    itemChecker.For(o => o.Number)
        .NotNullOrEmpty("Order number is required")
        .MustAsync(async (o, ct) => !await repo.OrderExistsAsync(o.Number!, ct),
            "Order number already exists");
});

Las claves se reescriben como:

  • Orders[0].Number
  • Emails[1].Length
8. Checkers async personalizados

Usa CheckerAsyncBase<T> cuando quieras un checker estrictamente async:

using Pitasoft.Error;

public sealed class UniqueEmailChecker : CheckerAsyncBase<User>
{
    public override IEnumerable<string> GetProperties() => [nameof(User.Emails)];

    public override async Task<ErrorCollection?> CheckAsync(
        User? instance,
        string propertyName,
        string displayName,
        CancellationToken cancellationToken = default)
    {
        if (instance is null) return null;

        await Task.Delay(10, cancellationToken);

        ErrorCollection? errors = null;
        foreach (var email in instance.Emails)
        {
            if (email == "taken@example.com")
            {
                (errors ??= new ErrorCollection()).Add(propertyName,
                    $"Email '{email}' is already registered");
            }
        }

        return errors is { HasErrors: true } ? errors : null;
    }
}
9. Métodos de extensión sobre objetos

Para validar rápido sin crear tipos de validator explícitos:

using Pitasoft.Validation.Extensions;

var errors1 = user.ValidateWithAttributes();
var errors2 = user.ValidateWithChecker(checker);

var errors3 = user.ValidateWithValidator(v =>
{
    v.For(u => u.Name).NotNullOrEmpty("Name is required");
    v.For(u => u.Age).Range(18, 99, "Invalid age");
});

var errors4 = await user.ValidateWithValidatorAsync(v =>
{
    v.For(u => u.Name)
        .NotNullOrEmpty("Name is required")
        .MustAsync(async (u, ct) => !await repo.ExistsAsync(u.Name!, ct),
            "Duplicated name");
}, cancellationToken);
10. Validación de propiedades concretas

La validación por propiedad es útil en formularios, asistentes y validación incremental.

var validator = new UserValidator(repo);

// Sync
var nameErrors = validator.ValidateProperty(user, nameof(User.Name));
var subsetErrors = validator.ValidateProperties(user, nameof(User.Name), nameof(User.Email));

// Async
var asyncNameErrors = await validator.ValidatePropertyAsync(user, nameof(User.Name), cancellationToken);
var asyncSubsetErrors = await validator.ValidatePropertiesAsync(
    user,
    new[] { nameof(User.Name), nameof(User.Email) },
    cancellationToken);

Con expresiones lambda:

using Pitasoft.Validation.Extensions;

var syncErrors = validator.ValidateProperty(user, u => u.Name);
var asyncErrors = await validator.ValidatePropertyAsync(user, u => u.Email, cancellationToken);

Catálogo fluente de reglas

El builder fluente incluye principalmente:

  • nulabilidad: NotNull, Null, NotNullOrEmpty
  • strings: MinLength, MaxLength, Length, LengthBetween, Matches, Email, StartsWith, EndsWith, Url
  • igualdad y conjuntos: Equal, NotEqual, In, NotIn
  • comparación: Range, ExclusiveBetween, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
  • colecciones: NotEmpty, MinItems, MaxItems
  • composición: When, Unless, WhenAsync, UnlessAsync, StopOnFirstFailure
  • delegados personalizados: Must, MustAsync

Formato de mensajes

Los mensajes de error soportan placeholders:

  • {0} = display name
  • {1} = valor actual de la propiedad

Ejemplos de claves de error

Claves típicas devueltas por la librería:

Escenario Clave de ejemplo
Validación de propiedad Name
Validación de objeto string.Empty
Propiedad de un validador hijo Address.City
Error de objeto de un hijo Address
Propiedad de un item de colección Orders[0].Number
Error de objeto de un item Orders[0].

Notas de comportamiento

  • RuleChecker<T> rechaza el registro de reglas async y lanza AsyncRuleRegistrationException
  • RuleCheckerAsync<T> puede ejecutar reglas sync y async
  • una llamada sync sobre un flujo async puede lanzar AsyncRuleInSyncValidationException
  • AsyncRuleBehavior.Skip habilita validación sync parcial
  • CheckerAsyncBase<T> es más estricto que RuleCheckerAsync<T>: sus entradas sync siempre lanzan excepción

Cuándo usar cada tipo

  • Usa DataAnnotationsChecker<T> si ya tienes atributos en el modelo
  • Usa RuleChecker<T> para reglas de negocio síncronas
  • Usa RuleCheckerAsync<T> si alguna regla necesita I/O async
  • Usa Validator<T> / ValidatorAsync<T> como servicio de validación para la aplicación
  • Usa CheckerAsyncBase<T> para validadores de dominio async que no deban ejecutarse de forma síncrona

Recomendaciones prácticas

  • Prefiere ValidatorAsync<T> cuando la validación pueda necesitar I/O.
  • Usa AsyncRuleBehavior.Skip solo cuando quieras validación sync parcial de forma explícita.
  • Mantén los validadores hijos centrados en un único tipo anidado o agregado.
  • Usa ForEach para validar colecciones en vez de replicar bucles manuales en la aplicación.
  • Trata la forma de ErrorCollection como comportamiento público si otras capas dependen de sus claves.
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 232 4/1/2026
6.0.4 187 3/31/2026
6.0.3 118 3/25/2026
6.0.2 109 3/25/2026
6.0.1 115 3/23/2026
5.1.5 118 3/17/2026
5.1.4 116 3/17/2026
5.1.3 140 3/5/2026
5.1.2 125 3/2/2026
5.1.1 119 3/2/2026
5.0.1 120 2/25/2026
4.3.0 488 11/20/2023
4.2.0 303 10/23/2023
4.1.0 766 11/18/2022
4.0.2 978 7/26/2022
4.0.0 833 7/26/2022
3.0.0 633 7/22/2022
2.0.0 673 6/26/2021
0.0.1 116 3/23/2026