A flexible and high-performance C# validation library designed to validate objects using multiple strategies, including Data Annotations, custom rules, and a fluent API.
$ dotnet add package Pitasoft.ValidationA flexible and high-performance C# validation library designed to validate objects using multiple strategies, including Data Annotations, custom rules, and a fluent API.
System.ComponentModel.DataAnnotations attributes.IChecker implementations.Items[0].Name).When / Unless).MustAsync and ValidateObjectAsync.AbstractValidator<T>.ErrorCollection format from the Pitasoft.Error library.Add the Pitasoft.Validation package to your project:
dotnet add package Pitasoft.Validation
Note: Depends on Pitasoft.Error.
The simplest way to start is using existing attributes on your models.
public class User
{
[Required]
[StringLength(50)]
public string Name { get; set; }
[Range(18, 99)]
public int Age { get; set; }
}
// Usage with Extension Methods (easiest)
var user = new User { Name = "", Age = 10 };
ErrorCollection? errors = user.ValidateWithAttributes();
// Or using a manual validator
var validator = new Validator<User>(new ValidationAttributeChecker<User>());
errors = validator.ValidateObject(user);
if (errors?.Any() == true)
{
// Handle errors
}
else
{
// Valid
}
Define rules programmatically without modifying your model classes.
// Usage with Extension Methods
var errors = user.ValidateWithValidator(v =>
{
v.For(u => u.Name)
.NotNullOrEmpty("Name is required")
.MinLength(2, "Name is too short")
.MaxLength(50, "Name is too long")
.Matches(@"^[a-zA-Z\s]+$", "Name must contain only letters");
v.For(u => u.Age)
.Range(18, 99, "Age must be between 18 and 99");
v.For(u => u.Email)
.Email("Invalid email address");
});
// Or using a manual validator
var validator = new Validator<User>();
validator.For(u => u.Name).NotNullOrEmpty("Required");
errors = validator.ValidateObject(user);
Validate complex object graphs by registering validators for child properties.
public class Order
{
public Address ShippingAddress { get; set; }
}
var addressValidator = new Validator<Address>(new ValidationAttributeChecker<Address>());
var orderValidator = new Validator<Order>();
// Registering child validator
orderValidator.RegisterChildValidator(o => o.ShippingAddress, addressValidator);
var errors = orderValidator.ValidateObject(order);
// Errors for address will have keys like "ShippingAddress.City"
All validation results are returned as an ErrorCollection, allowing for easy aggregation and consistent error handling across your application.
public void ProcessUser(User user)
{
var validator = CreateUserValidator();
if (!validator.TryValidate(user, out var errors))
{
// Handle errors (e.g., return HTTP 400 with the collection)
throw new ValidationException(errors);
}
}
| Method | Description |
|---|---|
NotNullOrEmpty(msg) | Validates that the value is not null or empty |
MinLength(min, msg) | Minimum string length |
MaxLength(max, msg) | Maximum string length |
Length(exact, msg) | Exact string length |
LengthBetween(min, max, msg) | String length within a range |
Matches(pattern, msg) | Value matches a regular expression |
Email(msg) | Valid email address format |
| Method | Description |
|---|---|
NotNull(msg) | Validates that the value is not null |
Null(msg) | Validates that the value is null |
Equal(expected, msg) | Value equals the expected value |
NotEqual(unexpected, msg) | Value does not equal the given value |
| Method | Description |
|---|---|
Range(min, max, msg) | Value is within the specified range (inclusive) |
GreaterThan(min, msg) | Value is strictly greater than the minimum |
GreaterThanOrEqual(min, msg) | Value is greater than or equal to the minimum |
LessThan(max, msg) | Value is strictly less than the maximum |
LessThanOrEqual(max, msg) | Value is less than or equal to the maximum |
| Method | Description |
|---|---|
NotEmpty(msg) | Collection is not empty |
MinItems(min, msg) | Collection has at least the specified number of elements |
MaxItems(max, msg) | Collection has at most the specified number of elements |
| Method | Description |
|---|---|
Must(predicate, msg) | Custom rule with access to the full object instance |
MustAsync(predicate, msg) | Async custom rule |
When(condition, configure) | Apply rules only when the condition is true |
Unless(condition, configure) | Apply rules only when the condition is false |
StopOnFirstFailure() | Stop evaluating rules for this property after the first failure |
All methods accept an optional code parameter to attach an error code for programmatic identification:
validator.For(u => u.Name).NotNullOrEmpty("Name is required", code: "NAME_REQUIRED");
| Placeholder | Value |
|---|---|
{0} | Visible property name (from DisplayAttribute if present) |
{1} | Current property value |
Define validators as reusable classes by inheriting from AbstractValidator<T>:
public class UserValidator : AbstractValidator<User>
{
protected override void Configure()
{
For(u => u.Name)
.NotNullOrEmpty("Name is required")
.MaxLength(50, "Name is too long");
For(u => u.Age)
.Range(18, 99, "Age must be between 18 and 99");
}
}
// Usage
var validator = new UserValidator();
var errors = validator.ValidateObject(user);
Apply rules only when a condition is met:
validator.For(u => u.CompanyName)
.When(u => u.IsCompany, v => v.NotNullOrEmpty("Company name is required for companies"));
validator.For(u => u.PersonalId)
.Unless(u => u.IsCompany, v => v.NotNullOrEmpty("Personal ID is required for individuals"));
Validate each element of a collection individually with indexed error paths:
public class Order
{
public List<OrderLine> Lines { get; set; }
}
var validator = new Validator<Order>();
validator.ForEach(o => o.Lines, line =>
{
line.For(l => l.Quantity).GreaterThan(0, "Quantity must be greater than zero");
line.For(l => l.ProductId).NotNullOrEmpty("Product is required");
});
var errors = validator.ValidateObject(order);
// Errors will have keys like "Lines[0].Quantity", "Lines[1].ProductId"
Use MustAsync for rules that require async operations (e.g., database lookups):
validator.For(u => u.Email)
.MustAsync(async u => !await emailService.ExistsAsync(u.Email), "Email already in use");
var errors = await validator.ValidateObjectAsync(user);
The Must method receives the full object instance, enabling validation that depends on multiple properties:
validator.For(u => u.YearsOfExperience)
.Must(u => u.YearsOfExperience >= u.MinRequiredExperience,
"Years of experience for {0} must be at least the minimum required");
Stop evaluating rules for a property after the first failure:
validator.For(u => u.Name)
.StopOnFirstFailure()
.NotNullOrEmpty("Name is required")
.MaxLength(50, "Name is too long");
Implement custom checkers to extend the validation engine. An IChecker can return structured errors with relative paths. This is useful for complex types or when you need to integrate with external validation libraries.
Validator<T> is designed for high performance:
DisplayAttribute).System.ComponentModel.DataAnnotations existentes.IChecker.Items[0].Name).When / Unless).MustAsync y ValidateObjectAsync.AbstractValidator<T>.ErrorCollection de la librería Pitasoft.Error.Agrega el paquete Pitasoft.Validation a tu proyecto:
dotnet add package Pitasoft.Validation
Nota: Depende de Pitasoft.Error.
La forma más simple de empezar es usando los atributos ya presentes en tus modelos.
public class User
{
[Required]
[StringLength(50)]
public string Name { get; set; }
[Range(18, 99)]
public int Age { get; set; }
}
// Uso con Métodos de Extensión (lo más fácil)
var user = new User { Name = "", Age = 10 };
ErrorCollection? errors = user.ValidateWithAttributes();
// O usando un validador manual
var validator = new Validator<User>(new ValidationAttributeChecker<User>());
errors = validator.ValidateObject(user);
if (errors?.Any() == true)
{
// Manejar errores
}
else
{
// Válido
}
Define reglas programáticamente sin modificar tus clases de modelo.
// Uso con Métodos de Extensión
var errors = user.ValidateWithValidator(v =>
{
v.For(u => u.Name)
.NotNullOrEmpty("El nombre es obligatorio")
.MinLength(2, "El nombre es demasiado corto")
.MaxLength(50, "El nombre es demasiado largo")
.Matches(@"^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$", "El nombre solo puede contener letras");
v.For(u => u.Age)
.Range(18, 99, "La edad debe estar entre 18 y 99");
v.For(u => u.Email)
.Email("Dirección de email no válida");
});
// O usando un validador manual
var validator = new Validator<User>();
validator.For(u => u.Name).NotNullOrEmpty("Obligatorio");
errors = validator.ValidateObject(user);
Valida grafos complejos registrando validadores para propiedades hijas.
public class Order
{
public Address ShippingAddress { get; set; }
}
var addressValidator = new Validator<Address>(new ValidationAttributeChecker<Address>());
var orderValidator = new Validator<Order>();
// Registrando validador hijo
orderValidator.RegisterChildValidator(o => o.ShippingAddress, addressValidator);
var errors = orderValidator.ValidateObject(order);
// Los errores del address tendrán claves como "ShippingAddress.City"
Todos los resultados se devuelven como ErrorCollection, lo que permite una agregación sencilla y un manejo de errores consistente en tu aplicación.
public void ProcessUser(User user)
{
var validator = CreateUserValidator();
if (!validator.TryValidate(user, out var errors))
{
// Manejar errores (p.ej., devolver HTTP 400 con la colección)
throw new ValidationException(errors);
}
}
| Método | Descripción |
|---|---|
NotNullOrEmpty(msg) | Valida que el valor no sea nulo ni vacío |
MinLength(min, msg) | Longitud mínima de la cadena |
MaxLength(max, msg) | Longitud máxima de la cadena |
Length(exact, msg) | Longitud exacta de la cadena |
LengthBetween(min, max, msg) | Longitud de la cadena dentro de un rango |
Matches(pattern, msg) | El valor coincide con una expresión regular |
Email(msg) | Formato de dirección de email válido |
| Método | Descripción |
|---|---|
NotNull(msg) | Valida que el valor no sea nulo |
Null(msg) | Valida que el valor sea nulo |
Equal(expected, msg) | El valor es igual al esperado |
NotEqual(unexpected, msg) | El valor es distinto al indicado |
| Método | Descripción |
|---|---|
Range(min, max, msg) | El valor está dentro del rango especificado (inclusive) |
GreaterThan(min, msg) | El valor es estrictamente mayor que el mínimo |
GreaterThanOrEqual(min, msg) | El valor es mayor o igual que el mínimo |
LessThan(max, msg) | El valor es estrictamente menor que el máximo |
LessThanOrEqual(max, msg) | El valor es menor o igual que el máximo |
| Método | Descripción |
|---|---|
NotEmpty(msg) | La colección no está vacía |
MinItems(min, msg) | La colección tiene al menos el número de elementos indicado |
MaxItems(max, msg) | La colección tiene como máximo el número de elementos indicado |
| Método | Descripción |
|---|---|
Must(predicate, msg) | Regla personalizada con acceso a la instancia completa del objeto |
MustAsync(predicate, msg) | Regla personalizada asíncrona |
When(condition, configure) | Aplica reglas solo cuando la condición es verdadera |
Unless(condition, configure) | Aplica reglas solo cuando la condición es falsa |
StopOnFirstFailure() | Detiene la evaluación de reglas de esta propiedad tras el primer fallo |
Todos los métodos aceptan un parámetro opcional code para adjuntar un código de error para identificación programática:
validator.For(u => u.Name).NotNullOrEmpty("El nombre es obligatorio", code: "NOMBRE_REQUERIDO");
| Marcador | Valor |
|---|---|
{0} | Nombre visible de la propiedad (desde DisplayAttribute si está presente) |
{1} | Valor actual de la propiedad |
Define validadores como clases reutilizables heredando de AbstractValidator<T>:
public class UserValidator : AbstractValidator<User>
{
protected override void Configure()
{
For(u => u.Name)
.NotNullOrEmpty("El nombre es obligatorio")
.MaxLength(50, "El nombre es demasiado largo");
For(u => u.Age)
.Range(18, 99, "La edad debe estar entre 18 y 99");
}
}
// Uso
var validator = new UserValidator();
var errors = validator.ValidateObject(user);
Aplica reglas solo cuando se cumple una condición:
validator.For(u => u.CompanyName)
.When(u => u.IsCompany, v => v.NotNullOrEmpty("El nombre de empresa es obligatorio para empresas"));
validator.For(u => u.PersonalId)
.Unless(u => u.IsCompany, v => v.NotNullOrEmpty("El DNI es obligatorio para personas físicas"));
Valida cada elemento de una colección individualmente con rutas de error indexadas:
public class Order
{
public List<OrderLine> Lines { get; set; }
}
var validator = new Validator<Order>();
validator.ForEach(o => o.Lines, line =>
{
line.For(l => l.Quantity).GreaterThan(0, "La cantidad debe ser mayor que cero");
line.For(l => l.ProductId).NotNullOrEmpty("El producto es obligatorio");
});
var errors = validator.ValidateObject(order);
// Los errores tendrán claves como "Lines[0].Quantity", "Lines[1].ProductId"
Usa MustAsync para reglas que requieren operaciones asíncronas (p.ej., consultas a base de datos):
validator.For(u => u.Email)
.MustAsync(async u => !await emailService.ExistsAsync(u.Email), "El email ya está en uso");
var errors = await validator.ValidateObjectAsync(user);
El método Must recibe la instancia completa del objeto, facilitando validaciones que dependan de múltiples campos:
validator.For(u => u.YearsOfExperience)
.Must(u => u.YearsOfExperience >= u.MinRequiredExperience,
"Los años de experiencia para {0} deben ser al menos el mínimo requerido");
Detiene la evaluación de reglas de una propiedad tras el primer fallo:
validator.For(u => u.Name)
.StopOnFirstFailure()
.NotNullOrEmpty("El nombre es obligatorio")
.MaxLength(50, "El nombre es demasiado largo");
Implementa validadores propios (IChecker) para extender el motor de validación. Un IChecker puede devolver errores estructurados con rutas relativas, ideal para integraciones o tipos complejos.
Validator<T> está diseñado para alto rendimiento:
DisplayAttribute).Sebastián Martínez Pérez
Copyright © 2020-2026 Pitasoft, S.L. Distribuido bajo la licencia LICENSE.txt incluida en este repositorio.