Skip to content

Getting started

The library reads and writes. This page walks through both: reading a file, importing it into your own type through a mapping, and writing one.

Install

dotnet add package TriasDev.Tabular

The package targets .NET 10 (and runs on .NET 11) and references nothing but the base class library. Version 0.7.2 is the last that also targets .NET 8.

Read a file

TabularFile.Open decides the format from the file's bytes — csv, xlsx, ods, a zip or tar archive of them, or a gzip-compressed file — and returns a cursor over its sheets and rows:

using TriasDev.Tabular;

using FileStream file = File.OpenRead("orders.xlsx");
using ITabularCursor cursor = TabularFile.Open(file, "orders.xlsx");

foreach (SheetInfo sheet in cursor.Sheets)
{
    cursor.MoveToSheet(sheet.Index);

    while (cursor.ReadRow())
    {
        ReadOnlySpan<RawCell> row = cursor.CurrentRow;   // typed cells: text, number, date, boolean
    }
}

A row is a view over a reused buffer: read what you need from it before the next ReadRow.

Import into your own type

The steps an upload goes through, from samples/TriasDev.Tabular.Samples.Import, which the build compiles — so the code on this page is code that works.

Declare the fields once. They are both the schema and the accessors that read a row:

// The fields, declared once: they build the schema and read the values.
TextImportField name = ImportField.Text("name").Require().MaxLength(100);
TextImportField country = ImportField.Text("country").ExactLength(2);
DateImportField signedOn = ImportField.Date("signed_on");
DecimalImportField amount = ImportField.Decimal("amount").Require();
ImportSchema schema = new() { Fields = [name, country, signedOn, amount] };

Profile the file and build a plan from its headers. In an application, a person confirms or changes the plan on a mapping screen; the profile is what that screen shows:

// 1. Profile the file, and build the plan from its headers.
FileProfile profile;
using (FileStream file = File.OpenRead(path))
using (ITabularCursor cursor = TabularFile.Open(file, Path.GetFileName(path)))
{
    profile = TabularAnalyzer.Analyze(cursor);
}

MappingPlan plan = MappingPlan.ByHeader(profile.Sheets[0], schema, culture: "de-DE");

Check the plan against the profile, before the file is read again:

// 2. Judge the plan against the profile, before reading the file again.
PrecheckResult check = MappingPrecheck.Check(plan, schema, profile);

foreach (PrecheckFinding finding in check.Findings)
{
    Console.WriteLine($"precheck: {finding.Severity} {finding.Code} on {finding.FieldName}");
}

if (!check.CanImport)
{
    return 1;
}

Import: typed rows, or errors that name their row, column and code:

// 3. Import: typed rows, or errors that point at their row and column.
using FileStream again = File.OpenRead(path);
using ImportRun<Customer> run = TabularImporter.Import(
    again, Path.GetFileName(path), plan, schema,
    row => new Customer(row[name]!, row[country], row[signedOn], row[amount]!.Value));

foreach (ImportOutcome<Customer> outcome in run.ReadRows())
{
    if (outcome.HasErrors)
    {
        foreach (RowError error in outcome.Errors)
        {
            Console.WriteLine($"row {error.RowNumber}, {error.FieldName}: {error.Code} ({error.RawValue})");
        }

        continue;
    }

    Customer customer = outcome.Value;
    Console.WriteLine(string.Create(
        System.Globalization.CultureInfo.InvariantCulture,
        $"imported {customer.Name}, {customer.Country}, {customer.SignedOn:yyyy-MM-dd}, {customer.Amount}"));
}

Console.WriteLine($"{run.Summary.RowsProduced} imported, {run.Summary.RowsFailed} failed");

Run it with dotnet run --project samples/TriasDev.Tabular.Samples.Import: the sample file has a date that is not one and an empty required amount, and both come back as row errors.

Write a file

The other direction, from samples/TriasDev.Tabular.Samples.Export, which the build compiles too.

Declare the export once. A column is a header and a lambda, whose type picks the column's type; a style rule picks a style per cell, and the sheet options give the header a style, freeze it and put an auto-filter on it:

private static readonly CellStyle Late = new() { Fill = CellColor.Parse("#FFC7CE") };

private static readonly CellStyle Header = new() { Fill = CellColor.Parse("#1F4E78"), Font = new CellFont { Bold = true, Color = CellColor.Parse("#FFFFFF") } };

// Declared once, kept in a static field, shared by any number of concurrent writes.
public static readonly TabularExport<Order> Export = TabularExport.For<Order>()
    .Column("Id", o => o.Id, width: 8)
    .Column("Customer", o => o.Customer, width: 28)
    .Column("Placed", o => o.Placed)
    .Column("Amount", o => o.Amount, width: 12)
    .Column("Status", o => o.Status, width: 10, style: status => status == "late" ? Late : null)
    .Column("Paid", o => o.Paid)
    .Sheet(new SheetOptions { HeaderStyle = Header, FreezeRows = 1, AutoFilter = true })
    .Build();

Write it into any stream — a file, a blob, an ASP.NET Core response; it need not seek. Chunks are the fast source, and memory holds a chunk and about a megabyte:

public static async Task<long> WriteChunksAsync(Stream stream, IAsyncEnumerable<IReadOnlyList<Order>> chunks, CancellationToken cancellationToken) =>
    await Export.WriteAsync(stream, TabularFormat.Xlsx, "Orders", chunks, cancellationToken: cancellationToken);

TabularFormat.Csv, Ods and Zip (a zip of csv sheets) are the same call; csv ignores the styles. The file is valid only once the write has returned: one that failed is incomplete, and you discard it. A value the format cannot hold exactly — a double past 15 significant digits, a date xlsx cannot store — fails the write with a TabularWriteException that names its sheet, row and column, rather than being rounded.

Run it with dotnet run --project samples/TriasDev.Tabular.Samples.Export: it writes each kind of file and reads it back.

Next

  • How it works — why analysis and import are two reads, what a profile measures, and how writing works
  • Importing — batches, rules of your own, translated fields, the precheck in depth
  • Exporting — the row-by-row writer, data by column, styles, layout, ASP.NET Core responses, failures
  • Formats — what csv, xlsx, ods, zip, tar and gzip files read as, and what each format holds when written