Apariencia
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
| Base | Uso |
|---|---|
Error<TSelf> | Jerarquía general sin código de dominio obligatorio |
DomainError<TSelf> | Datos o reglas de negocio incumplidas |
ProcessError | Fallos 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-ddEn 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.