Skip to content

Getting started

Install

dotnet add package TriasDev.Tabular

The package targets .NET 8 and .NET 10 and references nothing but the base class library.

Read a file

TabularFile.Open decides the format from the file's bytes — csv, xlsx, ods, or a zip archive of them — 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.
TextField name = ImportField.Text("name").Require().MaxLength(100);
TextField country = ImportField.Text("country").ExactLength(2);
DateField signedOn = ImportField.Date("signed_on");
DecimalField amount = ImportField.Decimal("amount").Require();
TargetSchema 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 = new 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.TargetFieldName}");
}

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)
{
    if (outcome.HasErrors)
    {
        foreach (RowError error in outcome.Errors)
        {
            Console.WriteLine($"row {error.RowNumber}, {error.TargetFieldName}: {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.

Next

  • How it works — why analysis and import are two reads, and what a profile measures
  • Importing — batches, rules of your own, translated fields, the precheck in depth
  • Formats — what csv, xlsx, ods and zip files read as