Pitasoft.Validation
0.0.1
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
<PackageReference Include="Pitasoft.Validation" Version="0.0.1" />
<PackageVersion Include="Pitasoft.Validation" Version="0.0.1" />
<PackageReference Include="Pitasoft.Validation" />
paket add Pitasoft.Validation --version 0.0.1
#r "nuget: Pitasoft.Validation, 0.0.1"
#:package Pitasoft.Validation@0.0.1
#addin nuget:?package=Pitasoft.Validation&version=0.0.1
#tool nuget:?package=Pitasoft.Validation&version=0.0.1
Pitasoft.Validation
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 validation —
MustAsyncrules with fullCancellationTokensupport throughout the call chain. - Async conditional rules —
WhenAsync/UnlessAsyncfor 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 validation —
When/Unlessfor context-dependent rules. - Stop on first failure — Per-property option to stop after the first failing rule.
- Reusable validators — Inherit from
Validator<T>orValidatorAsync<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 throwAsyncRuleInSyncValidationException.AsyncRuleBehavior.Skipis 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 theCheckObject/CheckObjectAsyncloop (e.g. bulk validation in a single round-trip).
Performance
- Compiled lambda expressions — property getters are compiled once and reused.
- Concurrent caching —
PropertyInfo, display names, validatable property lists, and compiled getters are cached inConcurrentDictionary. - Cached compiled regex —
Matches()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
MustAsynccon soporte completo deCancellationTokenen toda la cadena. - Reglas condicionales async —
WhenAsync/UnlessAsyncpara 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 condicional —
When/Unlesspara reglas dependientes del contexto. - Detener al primer fallo — Opción por propiedad para parar tras el primer error.
- Validadores reutilizables — Hereda de
Validator<T>oValidatorAsync<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 lanzarAsyncRuleInSyncValidationException.AsyncRuleBehavior.Skipes 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 bucleCheckObject/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é concurrente —
PropertyInfo, nombres visibles, listas de propiedades validables y getters compilados se almacenan enConcurrentDictionary. - Regex compilados y cacheados —
Matches()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 | Versions 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. |
-
net10.0
- Pitasoft.Error (>= 5.3.13)
-
net8.0
- Pitasoft.Error (>= 5.3.13)
-
net9.0
- Pitasoft.Error (>= 5.3.13)
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 |