Skip to content

Planes y composición de validaciones ​

Índice · Motivación · Anterior: árboles · Siguiente: diagnósticos

Un plan describe la política de validación de un caso de uso: qué condiciones aplicar, cómo se relacionan, a qué rutas pertenecen y cómo explicar sus incumplimientos. Se configura una vez y se ejecuta con candidatos y contextos distintos.

Problema que resuelve ​

Una regla aislada no conoce el objeto completo ni produce por sí sola un reporte para la aplicación. Sin un plan, cada consumidor tendría que coordinar manualmente selectores, orden, dependencias, códigos y mensajes. Esa repetición facilita que dos endpoints apliquen la misma regla de manera diferente.

Usa un plan cuando necesites validar un caso de uso completo u obtener ValidationReport. No hace falta para una comprobación booleana aislada. Tampoco debe almacenar estado de una pantalla: para coordinar varias ediciones utiliza una sesión de formulario.

La entrada recomendada es Validation.For<T>() para planes locales y Validation.For<T, C>() para planes contextuales. Primero define o registra las reglas; después compón el plan y reutilízalo con candidatos distintos.

Una regla completa ​

IValidationDefinition<T, C> reúne Definition y Diagnostic. ValidationRule.For construye esa pareja a partir de una condición, un identificador, un código y un mensaje. Admite una regla IRule<T>, un predicado local o un predicado contextual. ForAsync ofrece variantes locales y contextuales mediante delegados asíncronos.

csharp
using Greencore.platform.rules;
using Greencore.platform.validators;

var required = ValidationRule.For("text.required", new RequiredRule<string?>(), "email.required", "El correo es obligatorio.");
var format = ValidationRule.For("email.format", new EmailRule(), "email.invalid", "El correo no es válido.");

var plan = Validation.For<Customer>()
    .Field(x => x.Email, field => field.Sequence(sequence => sequence
        .Add(required)
        .Add(format)))
    .Build();

var report = plan.Validate(new Customer("incorrecto"));

public sealed record Customer(string? Email);

Para las reglas habituales, el mismo plan puede escribirse sin crear definiciones por separado:

csharp
var plan = Validation.For<Customer>()
    .Field(x => x.Email, field => field.Sequence(sequence => sequence
        .Required("El correo es obligatorio.")
        .Email("El correo no es válido.")))
    .Build();

Los atajos generan la definición internamente y conservan la semántica de Add y Sequence. Must y MustAsync cubren predicados propios; el parámetro opcional code permite fijar el código público del diagnóstico. Para texto, MinLength y MaxLength expresan el límite sin un booleano de dirección. Length se mantiene cuando la dirección se determina en tiempo de ejecución.

Dentro de Field(x => x.Email, ...), el candidato de las reglas es string?. Una regla definida sobre Customer debe registrarse al nivel del candidato completo, o reescribirse para recibir el valor del campo.

Independencia y dependencia ​

Para el caso frecuente de reglas dependientes de una propiedad, usa FieldSequence(x => x.Email, rules => rules.Required().Email()). Es equivalente a envolver Sequence dentro de Field, con las mismas rutas y ejecución. Las reglas comunes ofrecen mensajes predeterminados en español; las sobrecargas existentes permiten personalizarlos. Consulta el inicio rápido.

Componer no significa solamente agrupar reglas. La composición determina qué trabajo tiene sentido realizar y cómo interpretar el resultado. Usa ramas independientes cuando deseas reunir varios incumplimientos en una ejecución; usa dependencias cuando una comprobación carece de sentido o resulta costosa sin el éxito de otra.

Add(a).Add(b) registra dos ramas independientes. Con la configuración predeterminada ambas se evalúan aunque la primera falle. Require es un alias de Add; el nombre no cambia esa semántica.

Add(a).Then(b) reemplaza la última rama por un prerrequisito que relaciona a con b. Un nuevo Then(c) depende de la cadena anterior completa. Sin embargo, Add(a).Add(b).Then(c) hace depender c de b, no de a. Esta forma permanece para compatibilidad; los ejemplos nuevos expresan dependencias con Sequence y Add.

Sequence(s => s.Add(a).Add(b).Add(c)) expresa que cada rama depende del éxito de todas las anteriores. Es la opción recomendada para presencia, formato y consulta contextual. Una rama omitida detiene la cadena porque no equivale a éxito.

Then(callback) protege un grupo completo con la última rama. El builder exige una rama previa en el mismo nivel: empezar un callback de Field con Then lanza una excepción aunque haya reglas en otro campo o en otro builder.

Grupos y condiciones ​

AllOf(callback) exige todas las ramas aplicables. AnyOf(callback, code, message) acepta una alternativa y exige un diagnóstico de grupo. Not(callback, code, message) niega el AllOf que contiene el callback: si añades dos reglas, expresas la negación de su conjunción, no dos negaciones independientes.

When(condition, callback) aplica únicamente el contenido del callback. Tiene sobrecargas local y contextual. Unless(condition, callback) expone en el builder actual una condición local negada; para una condición contextual negada utiliza When((value, context) => !condition(value, context), callback).

Propiedades y objetos ​

Field obtiene una ruta de propiedades desde una expresión, compila el selector y adapta las condiciones. Admite x => x.Address.City, pero no añade navegación segura frente a null. La expresión debe ser una ruta de propiedades enraizada en el candidato; no una llamada arbitraria a un método, un indexador o un campo de C#.

OptionalObject omite sus reglas si el objeto seleccionado es nulo. RequiredObject produce un diagnóstico en la ruta del objeto y omite las propiedades si falta.

csharp
var cityRequired = ValidationRule.For("text.required", new RequiredRule<string?>(), "city.required", "La ciudad es obligatoria.");
var plan = Validation.For<Customer>()
    .OptionalObject(x => x.Address, address => address
        .Field(x => x.City, city => city.Add(cityRequired)))
    .Build();

public sealed record Address(string? City);
public sealed record Customer(Address? Address);

Para varios niveles opcionales, anida estas operaciones. Seleccionar directamente x => x.Address.City dentro de una protección de otro objeto no protege automáticamente Address.

Reutilizar planes ​

Include(plan) importa una composición del mismo candidato y contexto. Una extensión también adapta planes locales a un contexto. Al incluir, se conservan identidades de reglas y rutas, pero se regeneran todos los NodeId, incluso los que eran explícitos en el plan original. Dentro de Field, las rutas importadas reciben el prefijo del campo.

Incluir un plan local seguido de otra regla con Add no crea dependencias. Si la consulta debe esperar al éxito del plan incluido, usa una secuencia en ese nivel:

csharp
var fullPlan = Validation.For<Customer, CustomerContext>()
    .Sequence(sequence => sequence
        .Include(localPlan)
        .Add(customerUnique))
    .Build();

En este fragmento, localPlan valida Customer y customerUnique también recibe Customer. La consulta queda protegida por todo el plan local. Si necesitas dependencias distintas por campo, configura cada secuencia dentro de su Field.

Construcción y ejecución ​

ValidationBuilder es mutable. Ignorar el retorno de Add no descarta la llamada. Los callbacks se ejecutan durante la configuración y sus ramas se copian; Build() publica una estructura independiente de modificaciones posteriores al builder.

Los grupos vacíos, un plan vacío, identificadores duplicados y diagnósticos obligatorios ausentes se rechazan durante la configuración o construcción. Los identificadores automáticos son locales al plan y pueden cambiar al editarlo.

Comparte planes solo si sus condiciones, selectores y formateadores son seguros para concurrencia. La estructura inmutable no vuelve inmutable un servicio capturado ni el contenido de una evidencia.

Validate sirve para planes síncronos. ValidateAsync sirve para planes que contienen consultas y también puede ejecutar planes síncronos. El plan evalúa la composición completa cada vez; no incluye validación incremental ni caché de unicidad. Las excepciones técnicas y la cancelación se propagan al llamador.

Fuente: builder y plan.

Uso personal limitado. Uso comercial sujeto a autorización escrita.