Gesgocom.ConverterDocsGes 1.0.0

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:

  1. Inferir la estructura a partir de los tipos y metadatos del modelo
  2. Aplicar formato coherente
  3. Manejar las particularidades de cada formato de salida
  4. 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:

  1. Validar formato: Verifica que el formato sea tabular (Xlsx u Ods). Si no, lanza ArgumentException con mensaje indicando usar IDocumentExporter.
  2. Resolver columnas: Usa ModelAnalyzer.Analyze<T>() (modo atributos) o DefinitionAnalyzer.Analyze() (modo manual) para obtener List<ColumnDefinition>.
  3. Aplicar titulo: Si el tipo tiene [ExportDocument(Title="...")] o la definicion tiene Title, se usa como nombre de la hoja (si no se sobreescribio en TabularOptions).
  4. Uniformizar datos: Convierte cada fila (objeto tipado o diccionario) en una Func<string, object?> que dado un FieldId devuelve el valor. Esto desacopla completamente el formato del origen de datos.
  5. Delegar al formato: Enruta al exportador concreto (XlsxExporter para Xlsx, OdsExporter para Ods).

XlsxExporter (generador Excel con ClosedXML)

XlsxExporter es una clase internal que genera archivos .xlsx usando ClosedXML. Pasos de generacion:

  1. Cabeceras (fila 1): Escribe los DisplayName de cada columna con estilo corporativo — fondo #2c3e50 (azul oscuro), texto blanco, negrita, bordes, centrado. Altura de fila 22pt.

  2. Datos (fila 2 en adelante): Escribe cada celda con el tipo nativo de Excel correcto:

    • Integer/Decimal/Currency/Percentagecell.SetValue((double)valor) — Excel los trata como numeros reales, permitiendo sumas, filtros y ordenacion numerica.
    • Date/DateTime/Timecell.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 null o DBNull → celda vacia (Blank.Value).
  3. Porcentajes: Si el valor esta en escala 0-100 (>1 o <-1), divide entre 100 para que Excel lo muestre correctamente con formato %.

  4. 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).
  5. Autofiltros: Si AutoFilter=true y hay datos, aplica autofiltro en la cabecera.

  6. Freeze de cabecera: Si FreezeHeader=true, fija la fila 1 para scroll.

  7. Anchos de columna: Si la columna tiene Width > 0, usa ese ancho fijo. Si AutoFitColumns=true, ajusta al contenido con limites de 10-50 caracteres.

  8. Nombre de hoja: Sanitizado automatico — maximo 31 caracteres, sin caracteres invalidos (: \ / ? * [ ]).

  9. Salida: MemoryStream posicionado en 0 dentro de un ExportResult con ContentType application/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:

  1. Validar formato: Verifica que el formato sea documental (Docx u Odt). Si no, lanza ArgumentException con mensaje indicando usar ITabularExporter.
  2. Resolver columnas: Usa ModelAnalyzer.Analyze<T>() (modo atributos) o DefinitionAnalyzer.Analyze() (modo manual) para obtener List<ColumnDefinition>.
  3. Aplicar metadatos: Si el tipo tiene [ExportDocument(Title="...", Author="...")] o la definicion tiene Title/Author, se usan como metadatos del documento (si no se sobreescribieron en DocumentOptions).
  4. Uniformizar datos: Crea una Func<string, object?> que dado un FieldId devuelve el valor de la entidad unica. A diferencia del modo tabular (que produce IEnumerable<Func>), aqui es una sola funcion porque se exporta una sola entidad.
  5. Delegar al formato: Enruta al exportador concreto (DocxExporter para Docx, OdtExporter para Odt).

DocxExporter (generador Word con OfficeIMO)

DocxExporter es una clase internal que genera archivos .docx usando OfficeIMO.Word. Pasos de generacion:

  1. Propiedades del documento: Establece Title y Creator en las propiedades built-in del documento Word.

  2. Titulo: Si hay titulo configurado, lo renderiza como parrafo con estilo Heading1 y color corporativo #2c3e50.

  3. 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
  1. Salida: MemoryStream (via document.SaveAsMemoryStream()) posicionado en 0 dentro de un ExportResult con ContentType application/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 > 0Width * 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

  1. Si TabularOptions.Subtotals no es null, las filas se materializan como List<Dictionary<string, object?>> para permitir agrupacion
  2. Si GroupByFieldId esta definido, las filas se agrupan por el valor de esa columna
  3. Para cada grupo: se escriben las filas de datos + una fila de subtotal intermedia
  4. Si ShowGrandTotal = true, se anade una fila de gran total al final
  5. Si GroupByFieldId es 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.

Version Downloads Last updated
1.0.8 200 02/20/2026
1.0.7 3 02/19/2026
1.0.6 2 02/19/2026
1.0.5 2 02/19/2026
1.0.4 1 02/19/2026
1.0.3 1 02/19/2026
1.0.2 9 02/09/2026
1.0.1 1 02/08/2026
1.0.0 1 02/08/2026