Skip to content

Excel: estructura fija para importación ​

Una familia propia define una raíz, niveles de contexto y causas reconocibles. Para importar un libro usaremos hoja → fila → celda → validación. Para fallos al escribirlo definiremos una familia de procesos independiente.

Qué necesitas ​

Referencia el proyecto que expone Greencore.Domain.Errors (actualmente .NET 10). Los ejemplos usan imports implícitos de .NET y nulabilidad habilitada. Las clases se colocan en archivos de tu aplicación bajo su namespace.

La biblioteca de lectura o escritura de Excel la elige la aplicación. Estos ejemplos construyen diagnósticos; no abren archivos ni dependen de un proveedor de Excel.

Elegir la raíz y las relaciones ​

BaseUso
Error<TSelf>Jerarquía general sin código de dominio obligatorio
DomainError<TSelf>Datos o reglas de negocio incumplidas
ProcessErrorFallos al ejecutar una operación

En DomainReason<TParent, TSelf> y ProcessReason<TParent, TSelf>, el primer tipo es el padre permitido y el segundo es la razón concreta.

Importación: tipos completos ​

El siguiente bloque define la familia y los métodos de conveniencia. AddSheet, AddRow, AddCell, Required e InvalidFormat son parte del ejemplo, no métodos incorporados en Greencore.

csharp
using Greencore.Domain.Errors;

public sealed class ExcelImportError : DomainError<ExcelImportError>
{
    public string FileName { get; }

    public ExcelImportError(string fileName) : base("excel.validation_failed", $"El libro '{fileName}' contiene datos inválidos")
    {
        FileName = fileName;
    }

    public ExcelSheetReason AddSheet(string name) => AddReason(new ExcelSheetReason(name));
}

public sealed class ExcelSheetReason : DomainReason<ExcelImportError, ExcelSheetReason>
{
    public string Name { get; }

    public ExcelSheetReason(string name) : base("excel.sheet", $"Hoja {name}")
    {
        Name = name;
    }

    public ExcelRowReason AddRow(int number) => AddReason(new ExcelRowReason(number));
}

public sealed class ExcelRowReason : DomainReason<ExcelSheetReason, ExcelRowReason>
{
    public int Number { get; }

    public ExcelRowReason(int number) : base("excel.row", $"Fila {number}")
    {
        Number = number;
    }

    public ExcelCellReason AddCell(string address) => AddReason(new ExcelCellReason(address));
}

public sealed class ExcelCellReason : DomainReason<ExcelRowReason, ExcelCellReason>
{
    public string Address { get; }

    public ExcelCellReason(string address) : base("excel.cell", $"Celda {address}")
    {
        Address = address;
    }

    public ExcelCellReason Required() => WithReason(new ExcelRequiredReason());
    public ExcelCellReason InvalidFormat(string expectedFormat) => WithReason(new ExcelInvalidFormatReason(expectedFormat));
}

public sealed class ExcelRequiredReason : DomainReason<ExcelCellReason, ExcelRequiredReason>
{
    public ExcelRequiredReason() : base(DomainReasonCodes.Required, "El valor es obligatorio") { }
}

public sealed class ExcelInvalidFormatReason : DomainReason<ExcelCellReason, ExcelInvalidFormatReason>
{
    public string ExpectedFormat { get; }

    public ExcelInvalidFormatReason(string expectedFormat) : base(DomainReasonCodes.InvalidFormat, $"Formato esperado: {expectedFormat}")
    {
        ExpectedFormat = expectedFormat;
    }
}

Las validaciones son hijos de una celda, las celdas de una fila y las filas de una hoja. Para una importación de una sola hoja se puede omitir ese nivel y declarar ExcelImportError como padre de ExcelRowReason.

Construir el árbol después de validar ​

csharp
var error = new ExcelImportError("clientes.xlsx");
var sheet = error.AddSheet("Clientes");
var row = sheet.AddRow(12);

row.AddCell("B12").Required();
row.AddCell("C12").InvalidFormat("yyyy-MM-dd");

Console.WriteLine(error);

Estructura del diagnóstico:

text
El libro 'clientes.xlsx' contiene datos inválidos
└── Hoja Clientes
    └── Fila 12
        ├── Celda B12
        │   └── El valor es obligatorio
        └── Celda C12
            └── Formato esperado: yyyy-MM-dd

En una importación real, la aplicación lee los valores y agrega una razón solo cuando detecta una infracción. Conserva la hoja y fila en variables para agrupar las celdas afectadas; llamar de nuevo a AddRow(12) crea otro nodo, no busca el existente.

Comprobación de relaciones ​

csharp
// No compila: una celda requiere ExcelRowReason como padre.
// error.AddReason(new ExcelCellReason("B12"));

// No compila: Required requiere ExcelCellReason como padre.
// row.AddReason(new ExcelRequiredReason());

La raíz de dominio también conserva AddEntityReason<TEntity>(). La jerarquía propia añade relaciones y no elimina esa capacidad predefinida. Cada razón puede admitir nuevas razones que declaren ese padre; el diseño no es un esquema cerrado.

Interpretar las infracciones ​

Continuando el árbol anterior:

csharp
var traversal = new Greencore.Platform.Errors.Trees.TreeTraversal();

foreach (var issue in traversal.EnumerateNodes(error).OfType<ExcelRequiredReason>())
{
    var cell = issue.Parent!;
    var affectedRow = cell.Parent!;
    var affectedSheet = affectedRow.Parent!;

    Console.WriteLine($"{affectedSheet.Name}, fila {affectedRow.Number}, {cell.Address}: {issue.Code}");
}

Los padres son tipados. Aquí se usa ! porque todas las razones consultadas ya fueron adjuntadas al árbol. Un nodo recién construido puede tener Parent == null.

Para este ejemplo, el consumidor obtiene Clientes, fila 12, B12: validation.required. Puede crear un informe o señalar una celda sin analizar el mensaje. Para incluir otros tipos de validación se pueden recorrer sus razones o consultar los caminos con EnumerateLeafPaths.

Continuar ​

Consulta cómo diseñar una estructura fija para entender las decisiones del modelo y errores de escritura de Excel para describir fallos de ejecución.

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