Export

ArturRios.Data.Export turns any IEnumerable<T> into CSV, JSON, TXT, or MessagePack, over a stream or straight to a file….

ArturRios.Data.Export turns any IEnumerable<T> into CSV, JSON, TXT, or MessagePack, over a stream or straight to a file. ArturRios.Data.Export.Excel adds .xlsx as a separate add-on.

Both are standalone — they work with plain POCOs and depend on neither the relational core nor a database driver. They keep the same enveloped style as the rest of the family: every write returns a ProcessOutput, so a locked file or a serialization failure comes back as an error on the result rather than an unhandled exception.

Install

dotnet add package ArturRios.Data.Export
dotnet add package ArturRios.Data.Export.Excel        # optional — enables ExportFormat.Excel

Excel is a separate package on purpose: ClosedXML is a heavy dependency, and only apps that actually export spreadsheets should pay for it.

1. Register

using ArturRios.Data.Export.DependencyInjection;
using ArturRios.Data.Export.Excel.DependencyInjection;   // only with the add-on

builder.Services.AddExport();
builder.Services.AddExcelExport();                       // only with the add-on

AddExport() registers the IExporterFactory and the four core exporters. AddExcelExport() registers the Excel exporter and makes the factory resolve ExportFormat.Excel.

2. Write something

Inject IExporterFactory and resolve by format:

using ArturRios.Data.Export.Abstractions;
using ArturRios.Output;

public class ProductReport(IExporterFactory exporters)
{
    public async Task<ProcessOutput> WriteCsvAsync(IEnumerable<Product> products, string path)
    {
        var exporter = exporters.Resolve<Product>(ExportFormat.Csv);
        return await exporter.WriteToFileAsync(products, path);
    }
}

You can also inject a concrete exporter (CsvExporter<Product>, JsonExporter<Product>, …) when the format is fixed at compile time.

Each exporter has two methods:

MethodBehaviour
WriteAsync(data, stream, ct)writes to your stream — it is not disposed
WriteToFileAsync(data, path, ct)creates or replaces the file; a failed or cancelled write leaves an existing file untouched

3. Formats

ExportFormatExporterNotes
CsvCsvExporter<T>RFC 4180 quoting/escaping; configurable delimiter and encoding
JsonJsonExporter<T>a JSON array via System.Text.Json
TxtTxtExporter<T>one line per record; ToString() or a custom line selector
MessagePackMessagePackExporter<T>binary; contractless resolver, so no attributes required
ExcelExcelExporter<T> (add-on).xlsx via ClosedXML; requires AddExcelExport()

Resolving ExportFormat.Excel without the add-on registered throws a NotSupportedException naming the missing package and call. The core has no compile-time reference to the Excel package — the add-on drops a registration marker in the container, and the factory picks it up.

TxtExporter<T> has extra overloads taking a Func<T, string> line selector, for when ToString() isn’t the line you want.

4. Shaping columns

The columnar formats (CSV and Excel) build a column plan from the record’s public readable properties. Two attributes adjust it:

using ArturRios.Data.Export.Attributes;

public class Product
{
    [ExportColumn(Name = "Product name", Order = 1)]
    public string Name { get; set; } = string.Empty;

    [ExportColumn(Order = 2)]
    public decimal Price { get; set; }

    [ExportIgnore]
    public string InternalNotes { get; set; } = string.Empty;
}

Columns sort by Order ascending; unordered columns sort last. The plan compiles to delegate getters and is cached per type, so there’s no per-row reflection cost.

Json and MessagePack ignore the column map — they serialize the object graph as-is.

Values in CSV output are rendered culture-invariantly: null becomes empty, strings pass through, and anything IFormattable is formatted with CultureInfo.InvariantCulture. Excel writes booleans, DateTime, DateOnly, TimeSpan, TimeOnly and numbers as native cell values and renders everything else the same way (including NaN and infinities, which a cell cannot hold as a number).

CSV also guards against formula injection: a text value that starts with =, +, -, @, a tab or a carriage return is written with a leading ', so a spreadsheet shows it as text instead of running it. Numbers and other formatted values are never changed, so -5 stays a number. Set options.Csv.EscapeFormulas = false when the file is never opened in a spreadsheet and the exact text matters. Excel needs no such guard — it stores text as text.

5. Options

builder.Services.AddExport(options =>
{
    options.Csv.Delimiter = ';';
    options.Csv.IncludeHeader = true;
    options.Csv.Encoding = new UTF8Encoding(false);
    options.Csv.EscapeFormulas = true;    // default; prefixes spreadsheet formulas with '
    options.Json.WriteIndented = true;
    options.Txt.NewLine = "\n";
});

builder.Services.AddExcelExport(options =>
{
    options.SheetName = "Products";      // default "Sheet1"
    options.IncludeHeader = true;
    options.BoldHeader = true;
    options.AutoFitColumns = true;
});

Json and MessagePack also accept explicit SerializerOptions, used as-is when set. MessagePack otherwise defaults to the contractless standard resolver.

Notes

  • Excel numeric precision. The .xlsx format stores every number as an IEEE-754 double, so long/ulong beyond 2^53 and high-precision decimal values lose precision. Export those as strings if exactness matters.
  • Cancellation propagates as OperationCanceledException rather than being folded into the envelope — consistent with the rest of the toolkit.

Fidelity notes

  • Excel numbers are doubles. The xlsx format stores every number as an IEEE-754 double, so a long or ulong beyond 2^53, or a decimal carrying more precision than a double can hold, is written with the precision a double allows. Export such a column as text when the exact value matters.
  • CSV quoting is RFC 4180; the framing is not. A field is quoted when it holds the delimiter, a double quote or a line break, and an embedded quote is doubled — exactly as RFC 4180 prescribes. The delimiter itself comes from CsvOptions.Delimiter and lines end with the platform’s newline, so the output is strictly RFC 4180 only when the delimiter is a comma and the platform’s newline is CRLF.
  • Column order. Columns carrying [ExportColumn(Order = n)] come first, ascending. The rest follow in metadata (declaration) order; the name is only a tie-breaker for properties declared in different assemblies whose metadata tokens coincide.