Skip to content

Diagnósticos de dominio ​

DomainError representa un fallo esperado de negocio. Su jerarquía predefinida organiza las infracciones por entidad y propiedad.

Qué necesitas ​

Referencia Greencore.Domain.Errors e importa su namespace. Necesitas el tipo de entidad y haber detectado una infracción. No es obligatorio definir una raíz propia ni instalar un lector de archivos.

text
DomainError
└── EntityReason<TEntity>
    ├── EntityInvalidRuleReason<TEntity>
    └── EntityPropertyReason<TEntity, TMember>
        ├── EntityPropertyRequiredReason<TEntity, TMember>
        ├── EntityPropertyInvalidFormatReason<TEntity, TMember>
        └── EntityPropertyInvalidRuleReason<TEntity, TMember>

Construir el diagnóstico ​

Este ejemplo registra infracciones que el código llamador ya ha detectado:

csharp
using Greencore.Domain.Errors;

var error = DomainError.Create(code: "customer.invalid", message: "El cliente contiene datos inválidos");
var customer = error.AddEntityReason<Customer>(entityId: "42");

customer.AddPropertyReason(property: x => x.Email)
    .Required(text: "El correo es obligatorio");

customer.AddPropertyReason(property: x => x.Age)
    .InvalidRule(rule: age => age >= 18, text: "La edad mínima es 18 años");

customer.AddInvalidRuleReason(code: "customer.blocked", message: "El cliente está bloqueado");

Console.WriteLine(error);

public sealed class Customer
{
    public string Email { get; init; } = "";
    public int Age { get; init; }
}

AddEntityReason<Customer>() permite omitir el identificador. AddPropertyReason infiere el tipo del miembro a partir de la expresión.

Parámetros obligatorios y opcionales ​

csharp
// entityId es opcional: identifica la instancia afectada, sin buscarla ni cargarla.
var identifiedEntity = new EntityReason<Customer>(entityId: "42");

// Sin identificador: EntityId queda en null.
var unidentifiedEntity = new EntityReason<Customer>();

// code y message son obligatorios: identifican y describen el fallo.
var error = DomainError.Create(code: "customer.invalid", message: "Cliente inválido");

// También puedes omitir entityId al agregar la entidad desde la raíz.
error.AddEntityReason<Customer>()
    // property es obligatorio: selecciona la propiedad afectada.
    .AddPropertyReason(property: x => x.Email)
    // text es opcional: personaliza el mensaje de la infracción.
    .Required(text: "El correo es obligatorio");

// Required() sin argumentos usa el mensaje predeterminado de la biblioteca.
unidentifiedEntity.AddPropertyReason(property: x => x.Email).Required();

El identificador es opcional porque existen un constructor y una sobrecarga sin argumentos. Si proporcionas entityId, debe contener texto: no puede ser nulo, vacío ni solo espacios. "42" se conserva en EntityId y aparece en el mensaje de la entidad; no tiene un significado reservado.

En InvalidRule(rule: ..., text: ..., code: ...), la expresión rule es obligatoria; text y code son opcionales. Si omites code, se usa validation.invalid_rule. En la variante InvalidRule(code: ..., message: ...), ambos argumentos son obligatorios.

Composición fluida con la sintaxis de dominio ​

La familia de dominio ofrece una composición especializada: WithEntityReason, WithPropertyReason, Required e InvalidRule expresan el significado de cada nivel. Con el tipo Customer del ejemplo anterior, puedes construir el mismo diagnóstico en una expresión:

csharp
using Greencore.Domain.Errors;

var error = DomainError.Create(code: "customer.invalid", message: "El cliente contiene datos inválidos")
    .WithEntityReason(new EntityReason<Customer>(entityId: "42")
        .WithPropertyReason(new EntityPropertyReason<Customer, string>(property: x => x.Email)
            .Required(text: "El correo es obligatorio"))
        .WithPropertyReason(new EntityPropertyReason<Customer, int>(property: x => x.Age)
            .InvalidRule(rule: age => age >= 18, text: "La edad mínima es 18 años"))
        .WithInvalidRuleReason(code: "customer.blocked", message: "El cliente está bloqueado"));

La anidación muestra entidad → propiedades → validaciones. WithEntityReason devuelve el error; WithPropertyReason y WithInvalidRuleReason devuelven la entidad. Por eso las dos propiedades y la regla de entidad quedan como hermanas. Required e InvalidRule devuelven la propiedad sobre la que registran la infracción.

Esta sintaxis especializada evita construir manualmente cada razón de validación y repetir sus códigos. Como en el ejemplo anterior, registra infracciones ya detectadas; no evalúa las reglas.

Los árboles de diagnóstico personalizados bien diseñados pueden hacer más legible la composición de árboles de errores. La mejora viene de métodos con nombres que expresan el contexto y de valores de retorno consistentes: Add... para entrar al hijo y With... para continuar en el receptor. El tipado por sí solo no garantiza legibilidad; conviene conservar variables intermedias cuando una expresión tenga demasiados niveles.

En una familia propia puedes ofrecer la misma comodidad mediante métodos como AddSheet, AddRow, AddCell y Required, como muestra el ejemplo de Excel.

WithPropertyReason con tipos explícitos o inferidos ​

EntityReason<TEntity> ya conoce el tipo de entidad. El parámetro de WithPropertyReason<TMember> corresponde al tipo de la propiedad. Con el tipo Customer anterior, estas dos variantes construyen la misma estructura.

Con tipos explícitos:

csharp
// entityId es opcional; identifica al cliente afectado. Puedes usar new EntityReason<Customer>().
var entity = new EntityReason<Customer>(entityId: "42")
    .WithPropertyReason<string>(property: x => x.Email)
    .WithPropertyReason<int>(property: x => x.Age);

Con inferencia de tipos:

csharp
// entityId es opcional; identifica al cliente afectado. Puedes usar new EntityReason<Customer>().
var entity = new EntityReason<Customer>(entityId: "42")
    .WithPropertyReason(property: x => x.Email)
    .WithPropertyReason(property: x => x.Age);

Ambas llamadas agregan nodos de propiedades y devuelven la entidad, por lo que las propiedades quedan como hermanas. No agregan validaciones. Para entrar en una propiedad y registrar una infracción, usa AddPropertyReason.

Construir y retornar la raíz sin variables intermedias ​

DomainError.AddEntityReason<TEntity>() crea una razón de entidad sin identificador y la devuelve. Puedes continuar por sus propiedades, subir a la entidad con Up() y recuperar la raíz con Root(). No es obligatorio declarar variables para el error ni para sus razones.

Con el tipo Customer anterior:

csharp
using Greencore.Domain.Errors;
using Greencore.Platform.Errors;
using Greencore.Platform.Errors.Extensions;

static Error CreateCustomerError() =>
    DomainError.Create(code: "customer.invalid", message: "El cliente contiene datos inválidos")
        .AddEntityReason<Customer>()
            .AddPropertyReason(property: x => x.Email)
                .Required(text: "El correo es obligatorio")
            .Up()
            .AddPropertyReason(property: x => x.Age)
                .InvalidRule(rule: age => age >= 18, text: "La edad mínima es 18 años")
            .Up()
            .WithInvalidRuleReason(code: "customer.blocked", message: "El cliente está bloqueado")
        .Root();

Required e InvalidRule mantienen el receptor en la propiedad; cada Up() vuelve a la entidad para continuar con un hermano. Root() recorre los padres y devuelve la misma raíz como Error, sin copiarla. Por eso el método declara Error como retorno, aunque la instancia concreta es DomainError. Si necesitas el identificador de entidad, sustituye AddEntityReason<Customer>() por AddEntityReason<Customer>(entityId: "42").

La indentación ayuda a leer el árbol; la estructura la determinan los receptores y retornos. Root() requiere un nodo unido a una raíz y Up() requiere un padre; en esta cadena ambos requisitos se cumplen.

Razones y sintaxis ​

MétodoCódigo predeterminadoSignificado
Required(message)validation.requiredFalta un valor
InvalidFormat(message)validation.invalid_formatEl formato es incorrecto
InvalidRule(predicate, message)validation.invalid_ruleSe incumple una regla de propiedad
InvalidRule(code, message)Código proporcionadoRegla de propiedad identificada por la aplicación
AddInvalidRuleReason(code, message)Código proporcionadoRegla de entidad

Las expresiones de reglas se conservan; no se evalúan al construir las razones. Agrega cada razón solo cuando hayas comprobado el incumplimiento.

Interpretar el error ​

Continuando el ejemplo:

csharp
foreach (var entity in error.Reasons.OfType<EntityReason<Customer>>())
{
    foreach (var property in entity.Reasons.OfType<EntityPropertyReason<Customer, string>>())
    {
        foreach (var reason in property.Reasons.OfType<EntityPropertyRequiredReason<Customer, string>>())
            Console.WriteLine($"{entity.EntityId}: {property.Property} — {reason.Code}");
    }
}

El consumidor distingue un valor obligatorio por su tipo y código. La expresión identifica la propiedad; una interfaz de usuario puede establecer su propio mapeo entre esas expresiones y sus controles.

Ventaja frente a composición libre ​

Required() crea la razón y su código sin repetir convenciones de metadatos. Una selección como x => x.Email se verifica al compilar. Entidad, propiedad y validación tienen relaciones predefinidas que facilitan consultas uniformes.

Para razones exclusivas de una raíz, deriva de DomainError<TSelf>. Esta modalidad conserva los diagnósticos de entidades y añade razones declaradas para la raíz concreta. Véase familias propias.

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