Gesgocom.ConverterDocsGes 1.0.1
ConverterDocsGes
Componente de exportacion documental a partir de datos estructurados para .NET.
Idea del proyecto
Tienes datos en memoria (clases con propiedades tipadas, colecciones de registros) y quieres materializarlos en documentos reales, bien formateados y funcionales, listos para entregar o publicar.
No se trata de convertir entre formatos existentes. No hay un documento de origen. El origen son los datos de tu aplicacion y el destino es un documento profesional en el formato que necesites.
Modos de operacion
Modo tabular
Cuando tienes una coleccion de registros con campos homogeneos (una lista de entidades, un resultado de consulta, un listado de expedientes).
- Resultado natural: hoja de calculo Excel (.xlsx) u ODS (.ods)
- Cada campo se convierte en una columna, cada registro en una fila
- Los tipos se respetan (fechas como fechas, numeros como numeros, no todo como texto)
- Incluye cabeceras, formato legible y filtros si procede
Modo documento
Cuando tienes un unico registro o una estructura que tiene mas sentido como texto narrativo o ficha.
- Resultado natural: documento de texto Word (.docx) u ODT (.odt)
- Los campos se despliegan como secciones, parrafos, tablas internas o bloques etiqueta-valor
- Estructura semantica real (estilos de encabezado, parrafos, tablas)
Que lo hace util
El componente recibe un modelo (una clase, una coleccion) y se encarga de todo lo demas:
- Inferir la estructura a partir de los tipos y metadatos del modelo
- Aplicar formato coherente
- Manejar las particularidades de cada formato de salida
- Generar un archivo que se abra correctamente en cualquier suite ofimatica (Microsoft Office, LibreOffice, Google Docs)
El desarrollador no necesita saber como funciona internamente OpenXML ni el estandar ODF. Define su modelo, decora sus campos si quiere personalizar algo (cabeceras, orden, agrupaciones), elige formato de salida y obtiene un byte[] o un Stream.
Dos formas de definir la estructura
La libreria soporta dos formas complementarias de describir que campos exportar y como formatearlos:
1. Modo atributos — clases C# tipadas
Cuando tienes una clase con propiedades tipadas (el caso clasico), decoras con atributos y listo:
public class Expediente
{
[ExportColumn("Numero", Order = 1)]
public string Numero { get; set; }
[ExportColumn("Importe", DataType = ExportDataType.Currency)]
public decimal Importe { get; set; }
}
var result = await exporter.ExportAsync(expedientes, ExportFormat.Xlsx);
2. Modo definicion manual — datos dinamicos
Cuando la estructura se conoce en tiempo de ejecucion (datos de un JSON, una estructura dinamica en base de datos, un formulario configurable), no hay clase C# que decorar. Defines las columnas con una API fluent y pasas los datos como diccionarios:
var definition = new ExportDefinition("Contratos formalizados")
.Column("numero_expediente", "Numero de expediente", ExportDataType.Text)
.Column("fecha_adjudicacion", "Fecha adjudicacion", ExportDataType.Date)
.Column("importe_adjudicacion", "Importe", ExportDataType.Currency)
.Column("adjudicatario", "Adjudicatario", ExportDataType.Text)
.Column("es_publico", "Publico", ExportDataType.Boolean);
var result = await exporter.ExportAsync(datos, definition, ExportFormat.Xlsx);
Donde datos es un IEnumerable<IDictionary<string, object?>> — cada diccionario es una fila, las claves son los IDs de campo.
Ambos modos producen exactamente el mismo resultado. Internamente los dos se resuelven a una List<ColumnDefinition> que el exportador consume de forma uniforme.
Sistema de tipos de exportacion
El corazon de la libreria es un sistema de tipos semanticos que determina automaticamente como se formatea, alinea y representa cada campo. El desarrollador no necesita pensar en formatos de celda ni en estilos: el tipo de exportacion lo resuelve todo.
ExportDataType — tipos disponibles
| Tipo | Alineacion | Comportamiento por defecto | Ejemplo de salida |
|---|---|---|---|
Text |
Izquierda | Sin formato especial, texto literal | Juan Perez |
Integer |
Derecha | Sin decimales, separador de miles opcional | 1.250 |
Decimal |
Derecha | 2 decimales por defecto, configurable | 3.456,78 |
Currency |
Derecha | 2 decimales + simbolo de moneda | 1.250,00 EUR |
Percentage |
Derecha | Decimales configurables + simbolo % | 85,50 % |
Date |
Centro | Patron de fecha (defecto: dd/MM/yyyy) |
15/03/2025 |
DateTime |
Centro | Patron de fecha y hora (defecto: dd/MM/yyyy HH:mm) |
15/03/2025 14:30 |
Time |
Centro | Patron de hora (defecto: HH:mm) |
14:30 |
Boolean |
Centro | Texto configurable (defecto: Si / No) |
Si |
Deteccion automatica desde tipos C# (solo modo atributos)
La libreria infiere el ExportDataType a partir del tipo de la propiedad. No hay que hacer nada si los tipos C# ya son correctos:
| Tipo C# | ExportDataType detectado |
|---|---|
string |
Text |
int, long, short, byte (y nullable) |
Integer |
decimal, double, float (y nullable) |
Decimal |
DateTime, DateOnly (y nullable) |
Date |
DateTimeOffset (y nullable) |
DateTime |
TimeOnly, TimeSpan (y nullable) |
Time |
bool (y nullable) |
Boolean |
enum |
Text (usa .ToString()) |
Principio clave: sin atributos, un modelo con tipos correctos ya genera un documento bien formateado. Los atributos son para personalizar, no para que funcione.
Cuando necesitas sobreescribir el tipo
Hay casos donde el tipo C# no refleja la intencion de presentacion:
// Un decimal que realmente es moneda
[ExportColumn("Importe total", DataType = ExportDataType.Currency)]
public decimal ImporteTotal { get; set; }
// Un decimal que es un porcentaje (0.85 → "85,00 %")
[ExportColumn("IVA", DataType = ExportDataType.Percentage)]
public decimal PorcentajeIva { get; set; }
// Un int que no quieres con separador de miles (codigos, IDs)
[ExportColumn("Codigo", DataType = ExportDataType.Text)]
public int CodigoPostal { get; set; }
Parametros de formato por tipo
Cada tipo soporta parametros opcionales que afinan su comportamiento. Funcionan igual en atributos y en definicion manual:
En modo atributos:
[ExportColumn("Precio", DataType = ExportDataType.Decimal, Decimals = 4)]
public decimal PrecioPorUnidad { get; set; }
[ExportColumn("Total", DataType = ExportDataType.Currency, CurrencySymbol = "$")]
public decimal Total { get; set; }
[ExportColumn("Mes", DataType = ExportDataType.Date, Format = "MMMM yyyy")]
public DateOnly Periodo { get; set; }
[ExportColumn("Activo", TrueText = "Activo", FalseText = "Inactivo")]
public bool EstaActivo { get; set; }
En modo definicion manual:
var definition = new ExportDefinition("Presupuestos")
.Column("precio", "Precio unitario", ExportDataType.Decimal, c => c.Decimals(4))
.Column("total", "Total", ExportDataType.Currency, c => c.CurrencySymbol("$"))
.Column("periodo", "Mes", ExportDataType.Date, c => c.Format("MMMM yyyy"))
.Column("activo", "Estado", ExportDataType.Boolean, c => c.TrueText("Activo").FalseText("Inactivo"));
Cultura y localizacion
El formato numerico y de fechas respeta la cultura configurada:
var result = await exporter.ExportAsync(datos, ExportFormat.Xlsx, new TabularOptions
{
Culture = new CultureInfo("es-ES") // defecto: CultureInfo.CurrentCulture
});
| Aspecto | es-ES | en-US | de-DE |
|---|---|---|---|
| Separador decimal | , |
. |
, |
| Separador miles | . |
, |
. |
| Formato fecha | dd/MM/yyyy |
MM/dd/yyyy |
dd.MM.yyyy |
| Simbolo moneda | EUR |
$ |
EUR |
Atributos de exportacion (modo estatico)
Diseno del atributo unificado [ExportColumn]
Un unico atributo principal que consolida toda la configuracion de un campo:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ExportColumnAttribute : Attribute
{
/// Nombre visible en cabecera. Si no se indica, se usa el nombre de la propiedad.
public string? Name { get; }
/// Orden de aparicion (menor = primero). Sin definir = orden de declaracion.
public int Order { get; set; } = int.MaxValue;
/// Tipo de dato de exportacion. Sin definir = autodeteccion desde el tipo C#.
public ExportDataType DataType { get; set; } = ExportDataType.Auto;
/// Numero de decimales (solo para Decimal, Currency, Percentage).
public int Decimals { get; set; } = -1; // -1 = usar defecto del tipo
/// Patron de formato libre (solo para Date, DateTime, Time).
public string? Format { get; set; }
/// Simbolo de moneda (solo para Currency). Defecto: segun cultura.
public string? CurrencySymbol { get; set; }
/// Texto para valores true (solo para Boolean). Defecto: "Si".
public string? TrueText { get; set; }
/// Texto para valores false (solo para Boolean). Defecto: "No".
public string? FalseText { get; set; }
/// Ancho de columna en modo tabular (en caracteres). 0 = auto.
public int Width { get; set; } = 0;
/// Tipo de subtotal para esta columna (Sum, Count, Average, Min, Max). Defecto: None.
public SubtotalType Subtotal { get; set; } = SubtotalType.None;
public ExportColumnAttribute() { }
public ExportColumnAttribute(string name) => Name = name;
}
Atributos complementarios
/// Excluir una propiedad de la exportacion.
[ExportIgnore]
public string CampoInterno { get; set; }
/// Agrupar campos en secciones (modo documento).
[ExportGroup("Datos personales", Order = 1)]
public string Nombre { get; set; }
/// Metadatos del documento (se aplica a la clase).
[ExportDocument(Title = "Listado de expedientes", Author = "Sistema GES")]
public class Expediente { ... }
Definicion manual — API fluent (modo dinamico)
ExportDefinition — constructor de definiciones
Para datos cuya estructura se conoce en tiempo de ejecucion. La API es fluent y encadenable:
/// Definicion basica
var definition = new ExportDefinition("Titulo del documento")
.Column("campo_id", "Nombre visible", ExportDataType.Text)
.Column("campo_id", "Nombre visible", ExportDataType.Currency);
/// Definicion con configuracion avanzada por columna
var definition = new ExportDefinition("Presupuestos anuales")
.Column("ejercicio", "Ejercicio", ExportDataType.Integer, c => c
.Width(10))
.Column("descripcion", "Descripcion", ExportDataType.Text, c => c
.Width(40))
.Column("importe", "Presupuesto", ExportDataType.Currency, c => c
.Decimals(2)
.CurrencySymbol("EUR"))
.Column("porcentaje", "% Ejecucion", ExportDataType.Percentage, c => c
.Decimals(1))
.Column("fecha_aprobacion", "Aprobado", ExportDataType.Date, c => c
.Format("dd/MM/yyyy"))
.Column("publicado", "Publico", ExportDataType.Boolean, c => c
.TrueText("Si")
.FalseText("No"));
Agrupaciones en modo documento (manual)
var definition = new ExportDefinition("Ficha del cargo publico")
.Group("Datos personales", g => g
.Column("nombre", "Nombre completo", ExportDataType.Text)
.Column("nif", "DNI/NIF", ExportDataType.Text)
.Column("fecha_nacimiento", "Fecha nacimiento", ExportDataType.Date))
.Group("Retribuciones", g => g
.Column("salario_bruto", "Salario bruto", ExportDataType.Currency)
.Column("complementos", "Complementos", ExportDataType.Currency));
Datos como diccionarios
En modo manual, los datos se pasan como diccionarios donde las claves son los IDs de campo:
// --- Modo tabular: multiples registros ---
var datos = new List<Dictionary<string, object?>>
{
new() {
["ejercicio"] = 2024,
["descripcion"] = "Presupuesto general",
["importe"] = 5500000.00m,
["porcentaje"] = 0.875m,
["fecha_aprobacion"] = new DateTime(2024, 3, 15),
["publicado"] = true
},
new() {
["ejercicio"] = 2025,
["descripcion"] = "Presupuesto general",
["importe"] = 5800000.00m,
["porcentaje"] = 0.0m,
["fecha_aprobacion"] = null,
["publicado"] = false
}
};
var result = await tabularExporter.ExportAsync(datos, definition, ExportFormat.Xlsx);
// --- Modo documento: un unico registro ---
var ficha = new Dictionary<string, object?>
{
["nombre"] = "Juan Perez Garcia",
["nif"] = "12345678A",
["fecha_nacimiento"] = new DateOnly(1985, 3, 15),
["salario_bruto"] = 45000.00m,
["complementos"] = 3200.00m
};
var result = await documentExporter.ExportAsync(ficha, definition, ExportFormat.Docx);
Caso real: datos dinamicos desde JSON (ejemplo Transparencia)
Este es el caso de uso que motiva el modo manual. Tienes una tabla con la estructura de campos en un JSONB:
[
{"id": "numero_expediente", "tipo": "texto", "orden": 1,
"etiqueta": [{"etiqueta": "Numero de expediente", "ididioma": 1}],
"usarendocumento": true},
{"id": "fecha_adjudicacion", "tipo": "fecha", "orden": 6,
"etiqueta": [{"etiqueta": "Fecha de adjudicacion", "ididioma": 1}],
"usarendocumento": true},
{"id": "importe_adjudicacion", "tipo": "importe", "orden": 4,
"etiqueta": [{"etiqueta": "Importe de adjudicacion", "ididioma": 1}],
"usarendocumento": true},
{"id": "es_publico", "tipo": "booleano", "orden": 5,
"etiqueta": [{"etiqueta": "Es publico", "ididioma": 1}]}
]
Y los datos en otro JSONB:
{
"numero_expediente": "EXP-2024-0042",
"fecha_adjudicacion": "2024-06-15T00:00:00Z",
"importe_adjudicacion": 125000.50,
"es_publico": true
}
Conversion en el proyecto consumidor
El proyecto que usa la libreria (ej. TransparenciaAdmin) construye la definicion a partir del JSON. La libreria no conoce el formato de obligaciones, solo ofrece las herramientas:
// Este codigo vive en TransparenciaAdmin, no en ConverterDocsGes
public static ExportDefinition FromEstructura(JsonElement estructura, int idIdioma)
{
var definition = new ExportDefinition();
foreach (var campo in estructura.EnumerateArray()
.Where(c => c.TryGetProperty("usarendocumento", out var u) && u.GetBoolean())
.OrderBy(c => c.GetProperty("orden").GetInt32()))
{
var id = campo.GetProperty("id").GetString()!;
var tipo = campo.GetProperty("tipo").GetString()!;
var etiqueta = campo.GetProperty("etiqueta").EnumerateArray()
.First(e => e.GetProperty("ididioma").GetInt32() == idIdioma)
.GetProperty("etiqueta").GetString()!;
var dataType = tipo switch
{
"texto" or "memo" => ExportDataType.Text,
"fecha" => ExportDataType.Date,
"entero" or "ejercicio" => ExportDataType.Integer,
"decimal" => ExportDataType.Decimal,
"importe" => ExportDataType.Currency,
"booleano" => ExportDataType.Boolean,
"url" => ExportDataType.Text,
"html" => ExportDataType.Text, // stripear HTML
"seleccion-enteros" => ExportDataType.Text, // resolver etiqueta
"seleccion-texto" => ExportDataType.Text, // resolver etiqueta
_ => ExportDataType.Text
};
definition.Column(id, etiqueta, dataType);
}
return definition;
}
Y para convertir los datos:
// Tambien en TransparenciaAdmin
public static Dictionary<string, object?> FromDato(
JsonElement dato, JsonElement estructura, int idIdioma)
{
var dict = new Dictionary<string, object?>();
foreach (var campo in estructura.EnumerateArray())
{
var id = campo.GetProperty("id").GetString()!;
if (!dato.TryGetProperty(id, out var valor)) continue;
var tipo = campo.GetProperty("tipo").GetString()!;
dict[id] = tipo switch
{
"texto" or "memo" when campo.TryGetProperty("ididiomas", out var ml) && ml.GetBoolean()
=> valor.EnumerateArray()
.FirstOrDefault(e => e.GetProperty("ididioma").GetInt32() == idIdioma)
.GetProperty("etiqueta").GetString(),
"fecha" => DateTime.Parse(valor.GetString()!),
"entero" => valor.GetInt32(),
"decimal" => valor.GetDecimal(),
"importe" => valor.GetDecimal(),
"booleano" => valor.GetBoolean(),
"seleccion-enteros" => ResolverEtiquetaSeleccion(campo, valor.GetInt32(), idIdioma),
"seleccion-texto" => ResolverEtiquetaSeleccion(campo, valor.GetString()!, idIdioma),
_ => valor.ToString()
};
}
return dict;
}
Resultado: exportar obligaciones a Excel
// En un endpoint de la API de TransparenciaAdmin
[HttpGet("obligacion/{id}/export")]
public async Task<IActionResult> ExportObligacion(int id, [FromQuery] string formato)
{
var obligacion = await repo.GetObligacionAsync(id);
var registros = await repo.GetDatosAsync(id);
// 1. Construir definicion desde la estructura JSON
var definition = FromEstructura(obligacion.Estructura, idIdioma: 1);
// 2. Convertir cada dato JSONB a diccionario
var datos = registros.Select(r => FromDato(r.Dato, obligacion.Estructura, idIdioma: 1));
// 3. Exportar
var exportFormat = formato == "ods" ? ExportFormat.Ods : ExportFormat.Xlsx;
var result = await tabularExporter.ExportAsync(datos, definition, exportFormat, new TabularOptions
{
SheetName = obligacion.Nombre,
AutoFilter = true,
FreezeHeader = true,
Culture = new CultureInfo("es-ES")
});
return File(result.Stream, result.ContentType, result.FileName);
}
API publica completa
Interfaces
/// Exportar colecciones a formato tabular (xlsx/ods)
public interface ITabularExporter
{
// Modo atributos: desde clase tipada
Task<ExportResult> ExportAsync<T>(IEnumerable<T> data, ExportFormat format,
TabularOptions? options = null) where T : class;
// Modo manual: desde definicion + diccionarios
Task<ExportResult> ExportAsync(IEnumerable<IDictionary<string, object?>> data,
ExportDefinition definition, ExportFormat format,
TabularOptions? options = null);
}
/// Exportar entidad unica a formato documento (docx/odt)
public interface IDocumentExporter
{
// Modo atributos
Task<ExportResult> ExportAsync<T>(T data, ExportFormat format,
DocumentOptions? options = null) where T : class;
// Modo manual
Task<ExportResult> ExportAsync(IDictionary<string, object?> data,
ExportDefinition definition, ExportFormat format,
DocumentOptions? options = null);
}
Opciones
public class TabularOptions
{
public string? SheetName { get; set; } // Nombre de la hoja
public bool AutoFilter { get; set; } = true; // Autofiltros en cabeceras
public bool FreezeHeader { get; set; } = true; // Fijar fila de cabecera
public bool AutoFitColumns { get; set; } = true; // Ajustar anchos automaticamente
public CultureInfo? Culture { get; set; } // Cultura para formateo
public string? FileName { get; set; } // Nombre del archivo de salida
public SubtotalConfiguration? Subtotals { get; set; } // Subtotales (null = sin subtotales)
public HeaderImage? HeaderImage { get; set; } // Imagen de cabecera/logo
}
public class DocumentOptions
{
public string? Title { get; set; } // Titulo del documento
public string? Author { get; set; } // Autor
public DocumentLayout Layout { get; set; } = DocumentLayout.LabelValue;
public CultureInfo? Culture { get; set; }
public string? FileName { get; set; }
public HeaderImage? HeaderImage { get; set; } // Imagen de cabecera/logo
}
public enum DocumentLayout
{
LabelValue, // Etiqueta: Valor (tipo ficha)
Sections, // Agrupado por secciones con encabezados
Narrative // Parrafos fluidos
}
Resultado
public class ExportResult : IDisposable
{
public Stream Stream { get; } // El documento generado
public string ContentType { get; } // "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
public string FileName { get; } // "Expedientes_2025-03-15.xlsx"
}
Ejemplo modo atributos — sin decorar nada
public class Factura
{
public int Numero { get; set; } // → Integer, derecha, sin decimales
public string Cliente { get; set; } // → Text, izquierda
public DateTime Fecha { get; set; } // → Date, centro, dd/MM/yyyy
public decimal Total { get; set; } // → Decimal, derecha, 2 decimales
public bool Pagada { get; set; } // → Boolean, centro, Si/No
}
var result = await tabularExporter.ExportAsync(facturas, ExportFormat.Xlsx);
return File(result.Stream, result.ContentType, result.FileName);
Ejemplo modo atributos — personalizado
[ExportDocument(Title = "Listado de expedientes", Author = "Sistema GES")]
public class Expediente
{
[ExportColumn("N. Expediente", Order = 1, Width = 15)]
public string Numero { get; set; }
[ExportColumn("Fecha apertura", Order = 2, Format = "dd/MM/yyyy")]
public DateTime FechaApertura { get; set; }
[ExportColumn("Importe", Order = 3, DataType = ExportDataType.Currency, CurrencySymbol = "EUR")]
public decimal Importe { get; set; }
[ExportColumn("IVA", Order = 4, DataType = ExportDataType.Percentage, Decimals = 1)]
public decimal PorcentajeIva { get; set; }
[ExportColumn("Estado", Order = 5, TrueText = "Abierto", FalseText = "Cerrado")]
public bool Activo { get; set; }
[ExportIgnore]
public string HashInterno { get; set; }
}
Ejemplo modo documento — ficha con grupos
public class FichaCliente
{
[ExportGroup("Datos personales", Order = 1)]
[ExportColumn("Nombre completo")]
public string Nombre { get; set; }
[ExportGroup("Datos personales", Order = 1)]
[ExportColumn("DNI/NIF")]
public string Nif { get; set; }
[ExportGroup("Datos economicos", Order = 2)]
[ExportColumn("Saldo", DataType = ExportDataType.Currency)]
public decimal Saldo { get; set; }
}
var result = await documentExporter.ExportAsync(cliente, ExportFormat.Docx, new DocumentOptions
{
Title = "Ficha de cliente",
Layout = DocumentLayout.LabelValue
});
Documento generado:
FICHA DE CLIENTE
================
DATOS PERSONALES
────────────────
Nombre completo: Juan Perez Garcia
DNI/NIF: 12345678A
DATOS ECONOMICOS
────────────────
Saldo: 1.250,00 EUR
Analisis tecnico y propuesta
Formatos objetivo y librerias seleccionadas
| Formato | Tipo | Libreria propuesta | Licencia | Estado |
|---|---|---|---|---|
| .xlsx | Tabular | ClosedXML | MIT | Activa, madura |
| .ods | Tabular | Generador propio (ODF es ZIP+XML) | - | Controlado |
| .docx | Documento | Open XML SDK + OfficeIMO | MIT | Activa, Microsoft |
| .odt | Documento | Generador propio (ODF es ZIP+XML) | - | Controlado |
Justificacion de cada eleccion
Excel (.xlsx) → ClosedXML
- API de alto nivel, intuitiva y orientada a objetos
- Soporta tipos nativos (DateTime, numeros, booleanos), estilos, autofiltros, anchos automaticos
- MIT, sin restricciones comerciales
- Alternativas descartadas:
- NPOI: API mas verbosa (herencia de Java/POI), innecesaria si solo necesitamos .xlsx
- EPPlus: Licencia comercial desde v5, descartada por requisito open-source
- SpreadCheetah: Solo escritura streaming, sin formato rico
Word (.docx) → Open XML SDK + OfficeIMO
- Open XML SDK: SDK oficial de Microsoft, MIT, control total sobre el documento
- OfficeIMO: Wrapper de alto nivel sobre Open XML SDK (tambien MIT) que simplifica la creacion de documentos con parrafos, tablas, estilos, encabezados, etc.
- Alternativas descartadas:
- NPOI: API de menor nivel para Word que OfficeIMO
- FileFormat.Words: Menos maduro y menos comunidad
ODS (.ods) y ODT (.odt) → Generador propio ligero
- No existe una libreria .NET madura y activa para generar ODS/ODT de calidad
- AODL: Abandonada (ultima actualizacion hace anos), API obsoleta
- MaltReport: Orientada a templates, no a generacion desde datos
- ODF es un formato abierto y bien documentado: un archivo ODS/ODT es un ZIP con archivos XML (
content.xml,styles.xml,meta.xml,manifest.xml) que siguen el estandar OASIS OpenDocument v1.3 - Construir un generador propio ligero nos da control total, sin dependencias externas abandonadas, y el alcance es acotado (solo escritura, no lectura)
Arquitectura propuesta
ConverterDocsGes/
├── ConverterDocsGes.sln
└── ConverterDocsGes/
├── ConverterDocsGes.csproj
│
├── Attributes/ # Decoradores para modo estatico
│ ├── ExportColumnAttribute.cs # Atributo principal unificado
│ ├── ExportIgnoreAttribute.cs # [ExportIgnore]
│ ├── ExportGroupAttribute.cs # [ExportGroup("Seccion")]
│ └── ExportDocumentAttribute.cs # [ExportDocument(Title="...")]
│
├── Interfaces/ # Contratos publicos
│ ├── ITabularExporter.cs # Exportar colecciones → xlsx/ods
│ ├── IDocumentExporter.cs # Exportar entidad → docx/odt
│ └── IExportResult.cs # Resultado: Stream + ContentType + FileName
│
├── Models/ # Modelos, enums y definiciones
│ ├── ExportResult.cs # Implementacion de IExportResult
│ ├── ExportFormat.cs # Enum: Xlsx, Ods, Docx, Odt
│ ├── ExportDataType.cs # Enum: Auto, Text, Integer, Decimal, Currency...
│ ├── SubtotalType.cs # Enum: Sum, Count, Average, Min, Max
│ ├── SubtotalConfiguration.cs # Configuracion de subtotales y agrupacion
│ ├── HeaderImage.cs # Imagen de cabecera (byte[], MIME, dimensiones)
│ ├── ExportDefinition.cs # API fluent para definicion manual
│ ├── ColumnBuilder.cs # Builder fluent para configurar una columna
│ ├── TabularOptions.cs # Opciones modo tabular
│ ├── DocumentOptions.cs # Opciones modo documento
│ └── DocumentLayout.cs # Enum: LabelValue, Sections, Narrative
│
├── Core/ # Motor de reflexion, tipos y formato
│ ├── ColumnDefinition.cs # Definicion resuelta de una columna
│ ├── SubtotalAggregator.cs # Motor de agregacion para subtotales
│ ├── DataTypeResolver.cs # Resuelve ExportDataType desde tipo C# + atributo
│ ├── ValueFormatter.cs # Formatea valores segun DataType + cultura
│ ├── ModelAnalyzer.cs # Analiza clase tipada → List<ColumnDefinition>
│ └── DefinitionAnalyzer.cs # Analiza ExportDefinition → List<ColumnDefinition>
│
├── Exporters/ # Implementaciones por formato
│ ├── Tabular/
│ │ ├── TabularExporter.cs # Orquestador: resuelve columnas + delega al formato
│ │ ├── XlsxExporter.cs # ClosedXML → .xlsx (tipos nativos, estilos)
│ │ └── OdsExporter.cs # Generador propio → .ods (tipos nativos, estilos)
│ ├── Document/
│ │ ├── DocumentExporter.cs # Orquestador IDocumentExporter (resuelve columnas + delega)
│ │ ├── DocxExporter.cs # Generador Word con OfficeIMO (layouts, estilos)
│ │ └── OdtExporter.cs # Generador propio → .odt (layouts, estilos)
│ └── Odf/ # Generador ODF compartido (ODS + ODT)
│ └── OdfPackageBuilder.cs # Construccion del ZIP ODF (mimetype, manifest, meta, settings)
│
└── Extensions/ # Registro en DI y helpers
└── ServiceCollectionExtensions.cs # services.AddConverterDocs()
Flujo interno de una exportacion
MODO ATRIBUTOS MODO MANUAL
────────────── ───────────
Clase C# tipada ExportDefinition
│ │
▼ ▼
ModelAnalyzer.Analyze<T>() DefinitionAnalyzer.Analyze()
│ Lee propiedades │ Lee columnas definidas
│ Lee atributos │ (tipo ya es explicito)
│ Llama DataTypeResolver │
│ │
└──────────────┬────────────────────────┘
▼
List<ColumnDefinition>
│
▼
ValueFormatter.Format(valor, columnDef, culture)
│
▼
Exporter concreto (XlsxExporter, DocxExporter...)
│
▼
ExportResult { Stream, ContentType, FileName }
DataTypeResolver — logica de resolucion
1. Si el atributo/definicion especifica DataType != Auto → usar ese tipo
2. Si no (solo modo atributos), inferir del tipo C#:
- string → Text
- int/long/short/byte → Integer
- decimal/double/float → Decimal
- DateTime/DateOnly → Date
- DateTimeOffset → DateTime
- TimeOnly/TimeSpan → Time
- bool → Boolean
- enum → Text
- cualquier otro → Text (via .ToString())
3. Aplicar parametros si existen (Decimals, Format, etc.)
4. Si no, aplicar los defaults del tipo:
- Integer: Decimals=0, separador miles segun cultura
- Decimal: Decimals=2
- Currency: Decimals=2, simbolo segun cultura
- Percentage: Decimals=2, multiplicar por 100 si valor < 1
- Date: Format segun cultura (es-ES → "dd/MM/yyyy")
- Boolean: TrueText="Si", FalseText="No"
Dependencias (NuGet)
| Paquete | Version | Licencia | Uso |
|---|---|---|---|
| ClosedXML | 0.104.2 | MIT | Generacion XLSX |
| DocumentFormat.OpenXml | 3.3.0 | MIT | Generacion DOCX (bajo nivel) |
| OfficeIMO.Word | 1.0.20 | MIT | API alto nivel sobre OpenXml |
| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.2 | MIT | Registro en DI |
| Microsoft.Extensions.Logging.Abstractions | 10.0.2 | MIT | Logging |
| Microsoft.SourceLink.GitHub | 8.0.0 | MIT | Source linking (solo build) |
Sin dependencias propietarias. Todo MIT o Apache 2.0.
Fases de desarrollo
| Fase | Alcance | Estado |
|---|---|---|
| 1 | Core: atributos, ExportDefinition, DataTypeResolver, ValueFormatter, ModelAnalyzer, DefinitionAnalyzer | Completada |
| 2 | Modo tabular XLSX (ClosedXML) — ambos modos (atributos + manual) | Completada |
| 3 | Modo documento DOCX (OpenXML + OfficeIMO) — ambos modos | Completada |
| 4 | Modo tabular ODS (generador propio ODF) | Completada |
| 5 | Modo documento ODT (generador propio ODF) | Completada |
| 6 | Opciones avanzadas (subtotales, imagen de cabecera) | Completada |
Configuracion del proyecto
- Target: net10.0
- Package ID: Gesgocom.ConverterDocsGes
- Licencia: MIT
- GeneratePackageOnBuild: true
- Nullable: enable
- ImplicitUsings: enable
Manual tecnico de implementacion
Configuracion del origen NuGet privado
El paquete se publica en el servidor NuGet privado de Gesgocom. Antes de instalar, configura el origen:
dotnet nuget add source https://nuget.gesgocom.es/v3/index.json -n Gesgocom
Para verificar que el origen esta registrado:
dotnet nuget list source
Nota: Esta configuracion solo se hace una vez por maquina. Se almacena en el NuGet.Config global del usuario.
Instalacion
dotnet add package Gesgocom.ConverterDocsGes --source Gesgocom
O en el .csproj:
<PackageReference Include="Gesgocom.ConverterDocsGes" Version="1.0.0" />
Si el proyecto no encuentra el paquete, asegurate de que nuget.config del proyecto o de la maquina incluye el origen Gesgocom. Tambien puedes anadir un nuget.config en la raiz de tu solucion:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="Gesgocom" value="https://nuget.gesgocom.es/v3/index.json" />
</packageSources>
</configuration>
Registro en DI
// En Program.cs
using ConverterDocsGes.Extensions;
builder.Services.AddConverterDocs();
Esto registra ITabularExporter e IDocumentExporter como servicios Transient.
Namespaces principales
using ConverterDocsGes.Attributes; // [ExportColumn], [ExportIgnore], [ExportGroup], [ExportDocument]
using ConverterDocsGes.Models; // ExportDataType, ExportFormat, ExportDefinition, TabularOptions...
using ConverterDocsGes.Interfaces; // ITabularExporter, IDocumentExporter
using ConverterDocsGes.Core; // ColumnDefinition, ValueFormatter (uso avanzado)
using ConverterDocsGes.Extensions; // AddConverterDocs()
Referencia de atributos
[ExportColumn] — configura una propiedad para exportacion
Se aplica a propiedades. Si no se usa, la propiedad se exporta igualmente con autodeteccion.
| Parametro | Tipo | Defecto | Descripcion |
|---|---|---|---|
Name |
string? | nombre propiedad | Nombre visible en cabecera |
Order |
int | posicion en clase | Orden de aparicion (menor = primero) |
DataType |
ExportDataType | Auto | Tipo semantico (Auto = inferir de tipo C#) |
Decimals |
int | -1 (defecto tipo) | Decimales para Decimal/Currency/Percentage |
Format |
string? | segun cultura | Patron de formato para Date/DateTime/Time |
CurrencySymbol |
string? | segun cultura | Simbolo moneda para Currency |
TrueText |
string? | "Si" | Texto para true en Boolean |
FalseText |
string? | "No" | Texto para false en Boolean |
Width |
int | 0 (auto) | Ancho columna en caracteres |
Subtotal |
SubtotalType | None | Tipo de subtotal (Sum, Count, Average, Min, Max) |
[ExportIgnore] — excluye una propiedad
[ExportIgnore]
public string CampoInterno { get; set; }
[ExportGroup] — agrupa campos en secciones (modo documento)
| Parametro | Tipo | Descripcion |
|---|---|---|
Name |
string | Nombre de la seccion/grupo |
Order |
int | Orden del grupo (menor = primero) |
[ExportDocument] — metadatos del documento (se aplica a la clase)
| Parametro | Tipo | Descripcion |
|---|---|---|
Title |
string? | Titulo del documento |
Author |
string? | Autor del documento |
Referencia de ExportDataType
| Valor | Mapeo automatico desde C# | Alineacion | Formato defecto |
|---|---|---|---|
Auto |
(se resuelve segun el tipo C#) | - | - |
Text |
string, enum | Izquierda | .ToString() |
Integer |
int, long, short, byte | Derecha | N0 (sin decimales) |
Decimal |
decimal, double, float | Derecha | N2 (2 decimales) |
Currency |
(explicito) | Derecha | N2 + simbolo moneda |
Percentage |
(explicito) | Derecha | N2 + % (auto x100) |
Date |
DateTime, DateOnly | Centro | dd/MM/yyyy |
DateTime |
DateTimeOffset | Centro | dd/MM/yyyy HH:mm |
Time |
TimeOnly, TimeSpan | Centro | HH:mm |
Boolean |
bool | Centro | Si / No |
Referencia de ExportDefinition (API fluent)
// Crear definicion
var def = new ExportDefinition("Titulo");
// Anadir columna simple
def.Column("fieldId", "Nombre visible", ExportDataType.Text);
// Anadir columna con configuracion
def.Column("fieldId", "Nombre visible", ExportDataType.Currency, c => c
.Decimals(2)
.CurrencySymbol("EUR")
.Width(15));
// Anadir grupo de columnas (modo documento)
def.Group("Seccion", g => g
.Column("campo1", "Campo 1", ExportDataType.Text)
.Column("campo2", "Campo 2", ExportDataType.Date));
Metodos de ColumnBuilder
| Metodo | Aplica a | Descripcion |
|---|---|---|
.Decimals(int) |
Decimal, Currency, % | Numero de decimales |
.Format(string) |
Date, DateTime, Time | Patron de formato |
.CurrencySymbol(string) |
Currency | Simbolo o codigo ISO |
.TrueText(string) |
Boolean | Texto para true |
.FalseText(string) |
Boolean | Texto para false |
.Width(int) |
todos | Ancho columna (0=auto) |
.Group(string, int) |
todos | Asignar a grupo/seccion |
.Subtotal(SubtotalType) |
numeros, Count=todos | Tipo de subtotal para la columna |
Referencia de opciones
TabularOptions
| Propiedad | Tipo | Defecto | Descripcion |
|---|---|---|---|
SheetName |
string | "Datos" | Nombre de la hoja de calculo |
AutoFilter |
bool | true | Autofiltros en cabeceras |
FreezeHeader |
bool | true | Fijar fila de cabecera |
AutoFitColumns |
bool | true | Ajustar anchos al contenido |
Culture |
CultureInfo? | CurrentCulture | Cultura para formateo |
FileName |
string? | autogenerado | Nombre del archivo de salida |
Subtotals |
SubtotalConfiguration? | null (sin subtotales) | Configuracion de subtotales |
HeaderImage |
HeaderImage? | null (sin imagen) | Imagen de cabecera/logo |
DocumentOptions
| Propiedad | Tipo | Defecto | Descripcion |
|---|---|---|---|
Title |
string? | del atributo/def | Titulo del documento |
Author |
string? | del atributo/def | Autor del documento |
Layout |
DocumentLayout | LabelValue | Layout de presentacion |
Culture |
CultureInfo? | CurrentCulture | Cultura para formateo |
FileName |
string? | autogenerado | Nombre del archivo de salida |
HeaderImage |
HeaderImage? | null (sin imagen) | Imagen de cabecera/logo |
DocumentLayout
| Valor | Descripcion | |
|---|---|---|
LabelValue |
Tabla de dos columnas: Etiqueta | Valor (tipo ficha) |
Sections |
Agrupado por secciones con encabezados | |
Narrative |
Parrafos fluidos secuenciales |
Estructura de archivos del proyecto
ConverterDocsGes/
├── ConverterDocsGes.sln
├── README.md
└── ConverterDocsGes/
├── ConverterDocsGes.csproj
├── Attributes/
│ ├── ExportColumnAttribute.cs # [ExportColumn] — atributo principal unificado
│ ├── ExportIgnoreAttribute.cs # [ExportIgnore] — excluir propiedad
│ ├── ExportGroupAttribute.cs # [ExportGroup] — agrupar en secciones
│ └── ExportDocumentAttribute.cs # [ExportDocument] — metadatos de clase
├── Interfaces/
│ ├── ITabularExporter.cs # Contrato para exportar colecciones → xlsx/ods
│ └── IDocumentExporter.cs # Contrato para exportar entidad → docx/odt
├── Models/
│ ├── ExportDataType.cs # Enum: tipos semanticos (Text, Integer, Currency...)
│ ├── ExportFormat.cs # Enum: formatos de archivo (Xlsx, Ods, Docx, Odt)
│ ├── DocumentLayout.cs # Enum: layouts de documento (LabelValue, Sections...)
│ ├── SubtotalType.cs # Enum: tipos de subtotal (Sum, Count, Average, Min, Max)
│ ├── SubtotalConfiguration.cs # Configuracion de subtotales (agrupacion, etiquetas)
│ ├── HeaderImage.cs # Imagen de cabecera (byte[], MIME, dimensiones)
│ ├── ExportResult.cs # Resultado: Stream + ContentType + FileName
│ ├── ExportDefinition.cs # API fluent para definicion manual
│ ├── ColumnBuilder.cs # Builder fluent para parametros de columna
│ ├── TabularOptions.cs # Opciones para exportacion tabular
│ └── DocumentOptions.cs # Opciones para exportacion documental
├── Core/
│ ├── ColumnDefinition.cs # Definicion resuelta de columna (punto comun)
│ ├── SubtotalAggregator.cs # Motor de agregacion para subtotales
│ ├── DataTypeResolver.cs # Resuelve tipo C# → ExportDataType + defaults
│ ├── ValueFormatter.cs # Formatea valores segun tipo y cultura
│ ├── ModelAnalyzer.cs # Clase tipada → List<ColumnDefinition> (reflexion)
│ └── DefinitionAnalyzer.cs # ExportDefinition → List<ColumnDefinition>
├── Exporters/ # Implementaciones por formato
│ ├── Tabular/
│ │ ├── TabularExporter.cs # Orquestador ITabularExporter (resuelve columnas + delega)
│ │ ├── XlsxExporter.cs # Generador Excel con ClosedXML (tipos nativos, estilos)
│ │ └── OdsExporter.cs # Generador ODS con tipos nativos (generador propio)
│ ├── Document/
│ │ ├── DocumentExporter.cs # Orquestador IDocumentExporter (resuelve columnas + delega)
│ │ ├── DocxExporter.cs # Generador Word con OfficeIMO (layouts, estilos)
│ │ └── OdtExporter.cs # Generador ODT con layouts (generador propio)
│ └── Odf/ # Infraestructura ODF compartida (ODS + ODT)
│ └── OdfPackageBuilder.cs # Construccion del ZIP ODF (mimetype, manifest, meta, settings)
└── Extensions/
└── ServiceCollectionExtensions.cs # services.AddConverterDocs()
Detalles de implementacion — Exportacion tabular XLSX
TabularExporter (orquestador)
TabularExporter implementa ITabularExporter y actua como punto de entrada para exportaciones tabulares.
Su flujo interno para ambos modos de operacion:
- Validar formato: Verifica que el formato sea tabular (Xlsx u Ods). Si no, lanza
ArgumentExceptioncon mensaje indicando usarIDocumentExporter. - Resolver columnas: Usa
ModelAnalyzer.Analyze<T>()(modo atributos) oDefinitionAnalyzer.Analyze()(modo manual) para obtenerList<ColumnDefinition>. - Aplicar titulo: Si el tipo tiene
[ExportDocument(Title="...")]o la definicion tieneTitle, se usa como nombre de la hoja (si no se sobreescribio enTabularOptions). - Uniformizar datos: Convierte cada fila (objeto tipado o diccionario) en una
Func<string, object?>que dado unFieldIddevuelve el valor. Esto desacopla completamente el formato del origen de datos. - Delegar al formato: Enruta al exportador concreto (
XlsxExporterpara Xlsx,OdsExporterpara Ods).
XlsxExporter (generador Excel con ClosedXML)
XlsxExporter es una clase internal que genera archivos .xlsx usando ClosedXML. Pasos de generacion:
Cabeceras (fila 1): Escribe los
DisplayNamede cada columna con estilo corporativo — fondo#2c3e50(azul oscuro), texto blanco, negrita, bordes, centrado. Altura de fila 22pt.Datos (fila 2 en adelante): Escribe cada celda con el tipo nativo de Excel correcto:
Integer/Decimal/Currency/Percentage→cell.SetValue((double)valor)— Excel los trata como numeros reales, permitiendo sumas, filtros y ordenacion numerica.Date/DateTime/Time→cell.SetValue(DateTime)— Excel los almacena como fechas nativas, permitiendo filtros por rango de fechas.Boolean→ texto formateado ("Si"/"No", o texto personalizado) para legibilidad directa.Text→ string.- Valores
nulloDBNull→ celda vacia (Blank.Value).
Porcentajes: Si el valor esta en escala 0-100 (>1 o <-1), divide entre 100 para que Excel lo muestre correctamente con formato
%.Formatos de columna: Aplica sobre el rango de datos (no cabecera):
- Alineacion: segun
ColumnDefinition.Alignment(numeros a la derecha, fechas centradas, texto a la izquierda). - Formato numerico Excel:
#,##0(entero),#,##0.00(decimal),#,##0.00 [$EUR](moneda),0.00%(porcentaje),dd/mm/yyyy(fecha). - Conversion automatica de patrones .NET a Excel (MM → mm para meses).
- Alineacion: segun
Autofiltros: Si
AutoFilter=truey hay datos, aplica autofiltro en la cabecera.Freeze de cabecera: Si
FreezeHeader=true, fija la fila 1 para scroll.Anchos de columna: Si la columna tiene
Width > 0, usa ese ancho fijo. SiAutoFitColumns=true, ajusta al contenido con limites de 10-50 caracteres.Nombre de hoja: Sanitizado automatico — maximo 31 caracteres, sin caracteres invalidos (
: \ / ? * [ ]).Salida:
MemoryStreamposicionado en 0 dentro de unExportResultcon ContentTypeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
Detalles de implementacion — Exportacion documental DOCX
DocumentExporter (orquestador)
DocumentExporter implementa IDocumentExporter y actua como punto de entrada para exportaciones documentales.
Su flujo interno para ambos modos de operacion:
- Validar formato: Verifica que el formato sea documental (Docx u Odt). Si no, lanza
ArgumentExceptioncon mensaje indicando usarITabularExporter. - Resolver columnas: Usa
ModelAnalyzer.Analyze<T>()(modo atributos) oDefinitionAnalyzer.Analyze()(modo manual) para obtenerList<ColumnDefinition>. - Aplicar metadatos: Si el tipo tiene
[ExportDocument(Title="...", Author="...")]o la definicion tieneTitle/Author, se usan como metadatos del documento (si no se sobreescribieron enDocumentOptions). - Uniformizar datos: Crea una
Func<string, object?>que dado unFieldIddevuelve el valor de la entidad unica. A diferencia del modo tabular (que produceIEnumerable<Func>), aqui es una sola funcion porque se exporta una sola entidad. - Delegar al formato: Enruta al exportador concreto (
DocxExporterpara Docx,OdtExporterpara Odt).
DocxExporter (generador Word con OfficeIMO)
DocxExporter es una clase internal que genera archivos .docx usando OfficeIMO.Word. Pasos de generacion:
Propiedades del documento: Establece
TitleyCreatoren las propiedades built-in del documento Word.Titulo: Si hay titulo configurado, lo renderiza como parrafo con estilo
Heading1y color corporativo#2c3e50.Renderizado por layout: Segun
DocumentOptions.Layout, delega a uno de tres renderizadores:
Layout LabelValue (por defecto)
Tabla de 2 columnas (WordTableStyle.TableGrid), una fila por campo:
┌──────────────────┬──────────────────────────┐
│ Nombre: │ Juan Perez Garcia │
├──────────────────┼──────────────────────────┤
│ DNI/NIF: │ 12345678A │
├──────────────────┼──────────────────────────┤
│ Fecha alta: │ 15/03/2025 │
├──────────────────┼──────────────────────────┤
│ Saldo: │ 1.250,00 EUR │
└──────────────────┴──────────────────────────┘
- Columna 1:
DisplayName + ":"en negrita, Calibri 11pt - Columna 2: valor formateado con
ValueFormatter.FormatAsText(), Calibri 11pt
Layout Sections
Agrupa columnas por GroupName, ordena por GroupOrder:
DATOS PERSONALES ← Heading2, color #2c3e50
┌──────────────────┬──────────────────────────┐
│ Nombre: │ Juan Perez Garcia │
├──────────────────┼──────────────────────────┤
│ DNI/NIF: │ 12345678A │
└──────────────────┴──────────────────────────┘
DATOS ECONOMICOS ← Heading2, color #2c3e50
┌──────────────────┬──────────────────────────┐
│ Saldo: │ 1.250,00 EUR │
└──────────────────┴──────────────────────────┘
- Columnas con grupo →
GroupBy(GroupName).OrderBy(GroupOrder) - Columnas sin grupo → seccion final "Otros datos"
- Si TODAS las columnas son sin grupo → degrada a LabelValue plano (sin headings)
- Cada grupo: parrafo
Heading2+ tabla label-value con sus campos
Layout Narrative
Parrafos secuenciales, cada campo como dos parrafos:
Nombre:
Juan Perez Garcia
DNI/NIF:
12345678A
Saldo:
1.250,00 EUR
- Primer parrafo: etiqueta en negrita
- Segundo parrafo: valor formateado
- Sin tablas, formato narrativo simple
Estilo visual
- Color corporativo:
#2c3e50(mismo que cabeceras XLSX) para titulos y encabezados de seccion - Fuente: Calibri 11pt (default moderno de Office)
- Formato de valores: Todo via
ValueFormatter.FormatAsText()— en Word todo es texto, no hay tipos nativos como en Excel - Estilo de tabla:
WordTableStyle.TableGrid— bordes limpios sin fondo de color
- Salida:
MemoryStream(viadocument.SaveAsMemoryStream()) posicionado en 0 dentro de unExportResultcon ContentTypeapplication/vnd.openxmlformats-officedocument.wordprocessingml.document.
Detalles de implementacion — Exportacion tabular ODS
Generador propio vs libreria
A diferencia de XLSX (que usa ClosedXML), ODS se genera con un generador propio porque no existe una libreria .NET madura y activa para ODS. Un archivo ODS es un ZIP con archivos XML que siguen el estandar OASIS OpenDocument v1.2. La implementacion usa solo APIs del BCL (System.IO.Compression para el ZIP, System.Xml para generar XML con XmlWriter), sin ninguna dependencia NuGet adicional.
Estructura del ZIP ODF
archivo.ods (ZIP)
+-- mimetype (PRIMER archivo, SIN comprimir — regla critica ODF)
+-- META-INF/
| +-- manifest.xml (listado de archivos con tipos MIME)
+-- content.xml (datos tabulares + estilos automaticos)
+-- styles.xml (estilos globales: fuente, page layout)
+-- meta.xml (titulo, autor, fecha creacion)
+-- settings.xml (configuracion minima)
La regla mas importante del formato ODF: mimetype debe ser el primer archivo del ZIP y debe almacenarse sin comprimir (CompressionLevel.NoCompression). Esto permite a los detectores de tipo MIME identificar el formato leyendo solo los primeros bytes del archivo.
OdfPackageBuilder (infraestructura compartida)
OdfPackageBuilder es una clase internal que construye el paquete ZIP ODF. Es compartida entre ODS (Fase 4) y el futuro ODT (Fase 5). El exportador concreto genera content.xml y styles.xml, y el builder se encarga del resto (mimetype, manifest, meta, settings).
API fluent:
var stream = new OdfPackageBuilder("application/vnd.oasis.opendocument.spreadsheet")
.SetTitle("Nombre de la hoja")
.SetAuthor("Sistema GES")
.SetContentXml(contentXml)
.SetStylesXml(stylesXml)
.Build(); // → MemoryStream posicionado en 0
OdsExporter (generador ODS)
OdsExporter es una clase internal que sigue exactamente el mismo patron que XlsxExporter: constructor con ILogger, metodo Generate() con la misma firma.
Tipos de celda ODS
Cada tipo de dato se escribe con su tipo nativo ODS, igual que XLSX preserva tipos nativos de Excel:
| ExportDataType | office:value-type | Atributo de valor | Texto visible |
|---|---|---|---|
| Text | "string" |
(ninguno) | value.ToString() |
| Integer | "float" |
office:value="123" |
ValueFormatter.FormatAsText() |
| Decimal | "float" |
office:value="123.45" |
ValueFormatter.FormatAsText() |
| Currency | "currency" |
office:value + office:currency |
ValueFormatter.FormatAsText() |
| Percentage | "percentage" |
office:value="0.21" (fraccion) |
ValueFormatter.FormatAsText() |
| Date | "date" |
office:date-value="2025-03-15" |
ValueFormatter.FormatAsText() |
| DateTime | "date" |
office:date-value="2025-03-15T14:30:00" |
ValueFormatter.FormatAsText() |
| Time | "time" |
office:time-value="PT14H30M00S" |
ValueFormatter.FormatAsText() |
| Boolean | "string" |
(ninguno) | Si/No (texto formateado) |
| null/DBNull | (vacia) | table:table-cell sin atributos |
(vacio) |
Cada celda lleva un hijo <text:p> con el texto formateado visible. Esto es obligatorio en ODS incluso para celdas con tipo nativo.
Estilos generados
Los estilos se generan dinamicamente dentro de content.xml (seccion office:automatic-styles) basandose en las columnas definidas:
- Formatos numericos: Solo se generan para los tipos de dato que realmente se usan (NInt, NDec, NCur, NPct, NDate, NDateTime, NTime).
- Estilos de celda: Cabecera (fondo
#2c3e50, texto blanco, negrita — mismo estilo que XLSX), texto, entero, decimal, moneda, porcentaje, fecha, hora, booleano. - Estilos de columna: Ancho calculado por columna. Si
Width > 0→Width * 0.25 cm. Si no → ancho por defecto segun tipo (texto: 4cm, numeros: 3cm, fechas: 3.5cm). - Estilos de fila: Cabecera (0.65cm), datos (0.50cm con auto-height).
- Estilo de tabla: Visible, referencia a master-page "Default".
Porcentajes
ODS espera el valor como fraccion (0.21 = 21%). Se aplica la misma logica que en XLSX: si el valor es > 1 o < -1, se divide entre 100.
Salida
MemoryStream posicionado en 0 dentro de un ExportResult con ContentType application/vnd.oasis.opendocument.spreadsheet y extension .ods.
Detalles de implementacion — Exportacion documental ODT
Generador propio vs libreria
Al igual que ODS, ODT se genera con un generador propio porque no existe una libreria .NET madura para generar documentos ODT de calidad. Un archivo ODT es un ZIP con archivos XML (mismo estandar OASIS OpenDocument v1.2 que ODS). La implementacion reutiliza OdfPackageBuilder de la Fase 4 para la infraestructura ZIP, y solo necesita generar el content.xml y styles.xml especificos para documentos de texto.
OdtExporter (generador ODT)
OdtExporter es una clase internal que sigue exactamente el mismo patron que DocxExporter: constructor con ILogger, metodo Generate() con la misma firma (List<ColumnDefinition>, Func<string, object?>, DocumentOptions).
Layouts soportados
Los tres layouts del modo documento funcionan identicamente en ODT y DOCX:
Layout LabelValue (por defecto)
Tabla de 2 columnas con bordes (#cccccc), una fila por campo:
+------------------+--------------------------+
| Nombre: | Juan Perez Garcia |
+------------------+--------------------------+
| DNI/NIF: | 12345678A |
+------------------+--------------------------+
| Fecha alta: | 15/03/2025 |
+------------------+--------------------------+
| Saldo: | 1.250,00 EUR |
+------------------+--------------------------+
- Columna 1 (5cm): etiqueta en negrita, Calibri 11pt
- Columna 2 (12cm): valor formateado con
ValueFormatter.FormatAsText(), Calibri 11pt
Layout Sections
Agrupa columnas por GroupName, ordena por GroupOrder. Cada grupo genera un encabezado text:h nivel 2 (14pt, color corporativo #2c3e50) seguido de una tabla label-value. Si TODAS las columnas son sin grupo, degrada a LabelValue plano.
Columnas sin grupo se agrupan en seccion final "Otros datos".
Layout Narrative
Parrafos secuenciales, cada campo como dos parrafos:
Nombre: <- negrita, Calibri 11pt
Juan Perez Garcia <- normal, Calibri 11pt, margen inferior 0.3cm
DNI/NIF:
12345678A
Estilos generados
Los estilos se generan dentro de content.xml (seccion office:automatic-styles):
- Titulo: Calibri 18pt, negrita, color
#2c3e50 - Heading de seccion: Calibri 14pt, negrita, color
#2c3e50 - Tabla: Ancho 17cm, alineada a margenes
- Columna de etiquetas: 5cm
- Columna de valores: 12cm
- Celdas: Padding 0.1cm, borde
0.5pt solid #cccccc - Parrafo de etiqueta: Calibri 11pt, negrita
- Parrafo de valor: Calibri 11pt, normal
El styles.xml define estilos globales minimos: fuentes (Calibri, Arial), estilo default de parrafo y celda (Calibri 11pt), page layout A4 con margenes de 2cm, y estilos de heading con nombre.
Infraestructura compartida
OdtExporter reutiliza OdfPackageBuilder (creado en Fase 4) para el empaquetado ZIP. Solo genera content.xml y styles.xml; el builder se encarga de mimetype, manifest, meta y settings. El tipo MIME para ODT es application/vnd.oasis.opendocument.text.
Estilo visual
- Color corporativo:
#2c3e50(mismo que XLSX, ODS y DOCX) para titulos y encabezados - Fuente: Calibri 11pt (consistente con DOCX)
- Formato de valores: Todo via
ValueFormatter.FormatAsText()— en documentos de texto todo es texto, no hay tipos nativos como en hojas de calculo - Estilo de tabla: Bordes grises claros (
#cccccc), limpio y profesional
Salida
MemoryStream posicionado en 0 dentro de un ExportResult con ContentType application/vnd.oasis.opendocument.text y extension .odt.
Detalles de implementacion — Subtotales
SubtotalType
Enum que define el tipo de agregacion para una columna:
| Valor | Descripcion | Aplica a |
|---|---|---|
None |
Sin subtotal (defecto) | - |
Sum |
Suma de valores | Integer, Decimal, Currency, Percentage |
Count |
Conteo de valores no nulos | Cualquier tipo |
Average |
Promedio | Integer, Decimal, Currency, Percentage |
Min |
Valor minimo | Integer, Decimal, Currency, Percentage |
Max |
Valor maximo | Integer, Decimal, Currency, Percentage |
SubtotalConfiguration
Configura el comportamiento de subtotales en exportaciones tabulares:
public class SubtotalConfiguration
{
/// FieldId de la columna por la que agrupar.
/// Null = solo fila de gran total al final, sin subtotales intermedios.
public string? GroupByFieldId { get; set; }
/// Etiqueta para la fila de gran total.
public string GrandTotalLabel { get; set; } = "TOTAL";
/// Prefijo para filas de subtotal de grupo.
/// Ejemplo: "Subtotal " + "Departamento A" → "Subtotal Departamento A"
public string SubtotalLabel { get; set; } = "Subtotal";
/// Si se muestra la fila de gran total al final. Defecto: true.
public bool ShowGrandTotal { get; set; } = true;
}
SubtotalAggregator (motor de agregacion)
SubtotalAggregator es una clase internal que procesa filas y calcula agregados por grupo y gran total. Mantiene dos conjuntos de acumuladores:
- Agregados de grupo: se resetean al cambiar de grupo (
ResetGroup()) - Agregados de gran total: acumulan todas las filas
Solo las columnas con Subtotal != None se procesan. Los valores se extraen como decimal via Convert.ToDecimal(). Valores nulos se ignoran para Sum/Average/Min/Max pero no incrementan Count.
Flujo de subtotales en exportaciones tabulares
- Si
TabularOptions.Subtotalsno es null, las filas se materializan comoList<Dictionary<string, object?>>para permitir agrupacion - Si
GroupByFieldIdesta definido, las filas se agrupan por el valor de esa columna - Para cada grupo: se escriben las filas de datos + una fila de subtotal intermedia
- Si
ShowGrandTotal = true, se anade una fila de gran total al final - Si
GroupByFieldIdes null, solo se anade la fila de gran total (sin subtotales intermedios)
Estilo visual de subtotales
| Tipo de fila | Fondo | Texto | Peso |
|---|---|---|---|
| Subtotal grupo | #d5dbdb |
Negro | Negrita |
| Gran total | #2c3e50 |
Blanco | Negrita |
La etiqueta del subtotal se muestra en la primera columna de tipo texto. Los valores agregados se formatean usando el mismo ValueFormatter y configuracion de la columna original.
Ejemplo — Subtotales en modo atributos
public class Expediente
{
[ExportColumn("Departamento", Order = 1)]
public string Departamento { get; set; }
[ExportColumn("Importe", DataType = ExportDataType.Currency, Subtotal = SubtotalType.Sum)]
public decimal Importe { get; set; }
[ExportColumn("Registros", Subtotal = SubtotalType.Count)]
public int NumRegistros { get; set; }
}
var result = await exporter.ExportAsync(datos, ExportFormat.Xlsx, new TabularOptions
{
Subtotals = new SubtotalConfiguration
{
GroupByFieldId = "Departamento",
ShowGrandTotal = true,
GrandTotalLabel = "TOTAL GENERAL"
}
});
Ejemplo — Subtotales en modo manual
var def = new ExportDefinition("Presupuestos")
.Column("departamento", "Departamento", ExportDataType.Text)
.Column("importe", "Importe", ExportDataType.Currency, c => c
.Subtotal(SubtotalType.Sum))
.Column("porcentaje", "% Ejecucion", ExportDataType.Percentage, c => c
.Subtotal(SubtotalType.Average));
var result = await exporter.ExportAsync(datos, def, ExportFormat.Ods, new TabularOptions
{
Subtotals = new SubtotalConfiguration
{
GroupByFieldId = "departamento"
}
});
Detalles de implementacion — Imagen de cabecera
HeaderImage
Wrapper para la imagen de cabecera. Se proporciona como byte[] en memoria:
public class HeaderImage
{
public byte[] Data { get; }
public string MimeType { get; }
public double WidthCm { get; set; } = 4.0; // Ancho en cm
public double HeightCm { get; set; } = 2.0; // Alto en cm
public HeaderImage(byte[] data, string mimeType) { ... }
// Factorias convenientes
public static HeaderImage FromPng(byte[] data);
public static HeaderImage FromJpeg(byte[] data);
}
Soporte por formato
La imagen de cabecera se soporta en los 4 formatos de exportacion:
| Formato | Mecanismo |
|---|---|
| XLSX | worksheet.AddPicture() de ClosedXML, reserva 4 filas |
| ODS | draw:frame / draw:image dentro de table:shapes, imagen embebida en ZIP |
| DOCX | paragraph.AddImage() de OfficeIMO |
| ODT | draw:frame / draw:image en parrafo, imagen embebida en ZIP via OdfPackageBuilder |
La conversion de cm a pixeles usa el factor 37.8 px/cm (96 DPI).
Ejemplo — Imagen de cabecera
var logoBytes = await File.ReadAllBytesAsync("logo.png");
// En tabular (XLSX/ODS)
var result = await tabularExporter.ExportAsync(datos, ExportFormat.Xlsx, new TabularOptions
{
HeaderImage = HeaderImage.FromPng(logoBytes)
});
// En documental (DOCX/ODT)
var result = await documentExporter.ExportAsync(ficha, ExportFormat.Docx, new DocumentOptions
{
HeaderImage = new HeaderImage(logoBytes, "image/png") { WidthCm = 6, HeightCm = 3 }
});
No packages depend on Gesgocom.ConverterDocsGes.
.NET 10.0
- ClosedXML (>= 0.104.2)
- DocumentFormat.OpenXml (>= 3.3.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.2)
- OfficeIMO.Word (>= 1.0.20)