Skip to content

Ejemplo completo: correo obligatorio y único ​

Índice · Anterior: formularios

Este ejemplo es un programa de consola para .NET 10 que referencia platform.rules y platform.validators. El servicio simulado hace visible cuándo se ejecuta la consulta. Las reglas se crean una vez y reciben un contexto en cada ejecución.

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

var required = ValidationRule.For("text.required", new RequiredRule<string?>(), "customer.email.required", "El correo es obligatorio.");
var format = ValidationRule.For("email.format", new EmailRule(), "customer.email.invalid", "El correo no es válido.");
var unique = ValidationRule.ForAsync<string?, CustomerContext>(
    "email.unique",
    async (email, context, token) => !await context.Data.EmailExistsAsync(email!, context.ExcludedCustomerId, token),
    "customer.email.duplicate",
    "El correo ya está registrado.");

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

var data = new ExampleData();
foreach (var email in new string?[] { null, "incorrecto", "[email protected]", "[email protected]" })
{
    var candidate = new Customer(null, email);
    var context = new CustomerContext(data, candidate.Id);
    var report = await plan.ValidateAsync(candidate, context);
    Console.WriteLine($"Correo: {email ?? "(null)"}; válido: {report.IsValid}");
    foreach (var diagnostic in report.ForField((Customer x) => x.Email, DiagnosticView.Summary))
        Console.WriteLine($"  {diagnostic.Code}: {diagnostic.Message}");
}

public sealed record Customer(Guid? Id, string? Email);
public sealed record CustomerContext(ICustomerData Data, Guid? ExcludedCustomerId);

public interface ICustomerData
{
    ValueTask<bool> EmailExistsAsync(string email, Guid? excludedCustomerId, CancellationToken cancellationToken);
}

public sealed class ExampleData : ICustomerData
{
    private readonly Guid _existingId = Guid.Parse("11111111-1111-1111-1111-111111111111");

    public ValueTask<bool> EmailExistsAsync(string email, Guid? excludedCustomerId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        Console.WriteLine($"Consulta de unicidad: {email}");
        var exists = excludedCustomerId != _existingId && string.Equals(email, "[email protected]", StringComparison.OrdinalIgnoreCase);
        return ValueTask.FromResult(exists);
    }
}

Qué sucede en cada entrada ​

EntradaPresenciaFormatoConsultaResultado
nullFallaOmitidoOmitidacustomer.email.required
incorrectoCumpleFallaOmitidacustomer.email.invalid
[email protected]CumpleCumpleExistecustomer.email.duplicate
[email protected]CumpleCumpleNo existeVálido

unique recibe string?, porque está dentro del campo Email. La secuencia garantiza que solo se consulta después de superar presencia y formato. El operador ! en email! informa al compilador de esa garantía de composición; no agrega una validación en tiempo de ejecución.

ExcludedCustomerId permite excluir la propia fila durante una edición. La exclusión debe formar parte del contexto de la ejecución o de una condición sobre el candidato completo. No captures un identificador cambiante en un catálogo global de reglas.

El doble de datos usa una comparación concreta solo para demostrar el flujo. La aplicación real debe definir normalización y equivalencia del correo de manera consistente con su persistencia.

Plan local y plan de envío ​

Para validar mientras se escribe sin consultar servicios, puedes construir un plan local con presencia y formato. El envío utiliza el plan contextual completo. Ambos reutilizan las mismas definiciones required y format.

La inclusión de un plan no transforma automáticamente reglas posteriores en dependientes. Si agregas una regla contextual sobre el candidato completo, usa Sequence(s => s.Include(localPlan).Add(candidateRule)). Si validas por campo, conserva la secuencia de ese campo como en el ejemplo.

Decisiones frecuentes ​

NecesidadAPI apropiada
Comprobar un valor sin mensajesIRule<T>
Condición breve y propia de la aplicaciónValidationRule.For con predicado
Consulta externa con servicios por ejecuciónValidationRule.ForAsync<T, C>
Evitar consultas cuando faltan datosSequence
Acumular errores independientesVarios Add
Validar un objeto nullableOptionalObject o RequiredObject
Explicar un fallo con valores observadosWithEvidence o WithEvidenceAsync
Mostrar errores junto a un controlForField
Descartar respuestas de ediciones antiguasFormValidationSession
Cargar un selector dependienteOptions con DependsOn

Errores que conviene evitar ​

  • Dar por hecho que EmailRule exige presencia: acepta null.
  • Usar Add para condiciones que requieren que otra regla haya pasado.
  • Empezar un callback de Field con Then: ese builder todavía no tiene una rama previa.
  • Registrar una regla de Customer dentro de un campo cuyo valor es string?.
  • Usar Validate en un árbol que contiene hojas asíncronas.
  • Reutilizar un candidato mutable mientras una consulta sigue en curso.
  • Tratar Failed como un error funcional del campo.
  • Autorizar el envío con el último estado visual en lugar de validar la instantánea actual.
  • Omitir dependencias que el loader de opciones utiliza.

El repositorio también contiene un ejemplo ejecutable más amplio. Se ejecuta desde la raíz con:

powershell
dotnet run --project examples/platform.validators/examples.platform.validators.csproj

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