Saltar al contenido principal

Patrones avanzados

Este documento cubre casos de uso complejos: composición de validadores, herencia, validación condicional avanzada, contraseñas y colecciones anidadas.


Validadores anidados con SetValidator

public class CreateInvoiceValidator : AbstractValidator<CreateInvoiceRequest>
{
public CreateInvoiceValidator()
{
RuleFor(x => x.InvoiceNumber)
.NotEmpty()
.Matches(@"^INV-\d{4}-\d{6}$")
.WithMessage("El número de factura debe tener el formato INV-AAAA-XXXXXX.");

RuleFor(x => x.Customer)
.NotNull()
.SetValidator(new CustomerInfoValidator());
// Errores: Customer.Name, Customer.TaxId, Customer.Email

RuleFor(x => x.BillingAddress)
.NotNull()
.SetValidator(new AddressValidator());

RuleFor(x => x.Lines)
.NotEmptyCollection()
.WithMessage("La factura debe tener al menos una línea.");

RuleForEach(x => x.Lines)
.SetValidator(new InvoiceLineValidator());
// Errores: Lines[0].ProductCode, Lines[1].UnitPrice, etc.
}
}

Include para herencia de validadores

Include permite construir validadores en capas, donde cada capa añade reglas sin conocer las reglas de las demás.

// Validador base con campos comunes a creación y actualización
public class ProductBaseValidator : AbstractValidator<CreateProductRequest>
{
public ProductBaseValidator()
{
RuleFor(x => x.Name).NotEmpty().MinimumLength(3).MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0m).MaxDecimalPlaces(2);
}
}

// Validador de actualización admin — extiende las reglas base
public class AdminUpdateProductValidator : AbstractValidator<AdminUpdateProductRequest>
{
public AdminUpdateProductValidator()
{
Include(new ProductBaseValidator());

RuleFor(x => x.Id).GreaterThan(0)
.WithMessage("El ID del producto debe ser válido.");

RuleFor(x => x.ModifiedBy).NotEmpty()
.WithMessage("El nombre del admin que modifica es obligatorio.");

RuleFor(x => x.ModificationReason)
.NotEmpty()
.MinimumLength(20)
.MaximumLength(500);
}
}

Validación condicional compleja

Patrón: el tipo de entidad determina los campos obligatorios

public class CreateCustomerValidator : AbstractValidator<CreateCustomerRequest>
{
public CreateCustomerValidator()
{
RuleFor(x => x.CustomerType)
.NotEmpty()
.In(new[] { "individual", "company" })
.WithMessage("El tipo de cliente debe ser 'individual' o 'company'.")
.StopOnFirstFailure();

// Campos comunes
RuleFor(x => x.Email).NotEmpty().Email();
RuleFor(x => x.Phone).NotEmpty().PhoneNumber();

// Campos de persona física — solo si es individual
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage("El nombre es obligatorio para clientes individuales.")
.MaximumLength(100)
.When(x => x.CustomerType == "individual");

RuleFor(x => x.BirthDate)
.NotNull()
.PastDate()
.Must(d => d.HasValue && DateTime.Today.Year - d.Value.Year >= 18)
.WithMessage("El cliente debe ser mayor de edad.")
.When(x => x.CustomerType == "individual");

// Campos de empresa — solo si es company
RuleFor(x => x.CompanyName)
.NotEmpty()
.WithMessage("El nombre de la empresa es obligatorio.")
.MaximumLength(300)
.When(x => x.CustomerType == "company");

RuleFor(x => x.TaxId)
.NotEmpty()
.WithMessage("El CIF/NIF es obligatorio para empresas.")
.When(x => x.CustomerType == "company");
}
}

Validación avanzada de contraseñas

public class PasswordPolicyValidator : AbstractValidator<SetPasswordRequest>
{
public PasswordPolicyValidator()
{
RuleFor(x => x.NewPassword)
.NotEmpty().WithMessage("La contraseña es obligatoria.")
.MinimumLength(12).WithMessage("La contraseña debe tener al menos 12 caracteres.")
.MaximumLength(128)
.HasUppercase().WithMessage("Debe contener al menos una mayúscula.")
.HasLowercase().WithMessage("Debe contener al menos una minúscula.")
.HasDigit().WithMessage("Debe contener al menos un número.")
.HasSpecialChar().WithMessage("Debe contener al menos un carácter especial.")
.NoWhitespace().WithMessage("La contraseña no puede contener espacios.")
.StopOnFirstFailure();

RuleFor(x => x.ConfirmPassword)
.NotEmpty()
.EqualToProperty(x => x.NewPassword)
.WithMessage("Las contraseñas no coinciden.")
.StopOnFirstFailure();
}
}

Reutilización de reglas con métodos de extensión

public static class ValiValidationExtensions
{
public static IRuleBuilder<T, string> IsValidNationalId<T>(
this IRuleBuilder<T, string> builder)
{
return builder
.NotEmpty()
.Matches(@"^\d{9}$")
.WithMessage("El documento de identidad no tiene un formato válido.");
}

public static IRuleBuilder<T, decimal> IsValidPrice<T>(
this IRuleBuilder<T, decimal> builder,
decimal maxPrice = 999999.99m)
{
return builder
.GreaterThan(0m).WithMessage("El precio debe ser mayor que 0.")
.LessThanOrEqualTo(maxPrice).WithMessage($"El precio no puede superar {maxPrice:C}.")
.MaxDecimalPlaces(2).WithMessage("El precio no puede tener más de 2 decimales.");
}
}

// Uso en validadores
public class BankAccountValidator : AbstractValidator<BankAccount>
{
public BankAccountValidator()
{
RuleFor(x => x.HolderNationalId).IsValidNationalId();
RuleFor(x => x.Price).IsValidPrice(maxPrice: 50000m);
}
}

Switch/Case: Validación polimórfica

public class ShipmentValidator : AbstractValidator<ShipmentRequest>
{
public ShipmentValidator()
{
RuleFor(x => x.TrackingNumber)
.NotEmpty()
.Matches(@"^TRK-\d{10}$")
.WithMessage("El número de seguimiento debe tener el formato TRK-XXXXXXXXXX.");

RuleFor(x => x.ShippingType)
.NotEmpty()
.In(new[] { "home_delivery", "store_pickup", "locker" })
.StopOnFirstFailure();

RuleSwitch(x => x.ShippingType)
.Case("home_delivery", rules =>
{
rules.RuleFor(x => x.RecipientName).NotEmpty().MaximumLength(200);
rules.RuleFor(x => x.Street).NotEmpty().MaximumLength(200);
rules.RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}$");
})
.Case("store_pickup", rules =>
{
rules.RuleFor(x => x.StoreCode).NotEmpty().Matches(@"^STR-[A-Z0-9]{4}$");
})
.Case("locker", rules =>
{
rules.RuleFor(x => x.LockerId).NotEmpty().Matches(@"^LKR-\d{6}$");
rules.RuleFor(x => x.LockerAccessCode).NotEmpty().IsNumeric().LengthBetween(4, 8);
});
}
}

Siguientes pasos