ExcelReader.NET
1.4.1
See the version list below for details.
dotnet add package ExcelReader.NET --version 1.4.1
NuGet\Install-Package ExcelReader.NET -Version 1.4.1
<PackageReference Include="ExcelReader.NET" Version="1.4.1" />
<PackageVersion Include="ExcelReader.NET" Version="1.4.1" />
<PackageReference Include="ExcelReader.NET" />
paket add ExcelReader.NET --version 1.4.1
#r "nuget: ExcelReader.NET, 1.4.1"
#:package ExcelReader.NET@1.4.1
#addin nuget:?package=ExcelReader.NET&version=1.4.1
#tool nuget:?package=ExcelReader.NET&version=1.4.1
ExcelReader
High-performance Excel reading and writing for .NET 10. Reads .xlsx, .xlsb, .xls, and .csv; writes .xlsx, .xlsb, .xls, and .csv.
ExcelReader is built for streaming spreadsheet workloads where low allocations matter. It reads worksheet rows as lightweight ref struct values, resolves shared strings, recognizes date styles, handles sparse cells, and includes writers for producing .xlsx (Open XML), .xlsb (BIFF12), and .xls (BIFF8) workbooks.
Benchmarks
Benchmarks were run with BenchmarkDotNet v0.15.8 on Windows 10 (22H2), AMD Ryzen 7 5700X, .NET 10.0.9 (SDK 11.0.100-preview.4). Generated-data benchmarks use 50,000 rows. Raw results: tests/ExcelReader.Benchmarks/BenchmarkDotNet.Artifacts/results.
XLSX
Compares ExcelReader against established XLSX libraries on the same generated workbook shape.
| Scenario | ExcelReader | MiniExcel | Sylvan | SpreadCheetah |
|---|---|---|---|---|
| Cell-by-cell read | 11.650 ms, 16.80 KB | 149.472 ms, 209.02 MB | 35.843 ms, 1.89 MB | - |
| Cell-by-cell read async | 11.744 ms, 18.93 KB | - | - | - |
| Typed row parsing | 15.099 ms, 3.88 MB | 154.309 ms, 197.79 MB | 57.022 ms, 10.48 MB | - |
| Typed row parsing async | 14.620 ms, 3.88 MB | - | 58.538 ms, 10.51 MB | - |
| Workbook writing | 14.442 ms, 4.02 MB | 280.914 ms, 84.90 MB | - | 15.912 ms, 15.84 MB |
| Workbook writing, shared strings | 14.796 ms, 4.06 MB | - | - | - |
ExcelReader is ~12.8x faster than MiniExcel and ~3.1x faster than Sylvan for raw XLSX reads, allocating ~12,700x and ~115x less respectively. For typed parsing, it is ~10.2x faster than MiniExcel and ~3.8x faster than Sylvan. For XLSX writing, ExcelReader is ~1.1x faster than SpreadCheetah and allocates ~3.9x less memory; it is ~19.5x faster than MiniExcel and allocates ~21x less.
XLSB (BIFF12)
| Scenario | ExcelReader |
|---|---|
| Cell-by-cell read | 4.418 ms, 16.43 KB |
| Cell-by-cell read async | 4.781 ms, 18.98 KB |
| Typed row parsing | 6.870 ms, 3.88 MB |
| Typed row parsing async | 6.928 ms, 3.88 MB |
| Workbook writing | 7.524 ms, 4.02 MB |
| Workbook writing, shared strings | 7.382 ms, 4.06 MB |
XLSB is the fastest generated Excel format in these results: raw reads are ~2.6x faster than XLSX reads, typed parsing is ~2.2x faster than XLSX parsing, and writing is ~1.9x faster than XLSX writing. The XLSB writer is also ~2.1x faster than SpreadCheetah on this benchmark while allocating ~75% less memory.
XLS (BIFF8)
| Scenario | ExcelReader | Sylvan |
|---|---|---|
| Cell-by-cell read | 4.267 ms, 3.69 KB | 5.510 ms, 1,717.98 KB |
| Cell-by-cell read async | 4.270 ms, 3.76 KB | - |
| Workbook writing | 5.422 ms, 16.03 MB | - |
ExcelReader is ~1.3x faster than Sylvan for generated XLS reads while allocating ~466x less memory. The XLS writer is ~2.7x faster than the XLSX writer in this benchmark, but it allocates more because the BIFF8/OLE container is assembled in memory.
CSV
| Scenario | ExcelReader | Sep | Sylvan.Data.Csv | CsvHelper |
|---|---|---|---|---|
| Cell-by-cell read | 5.228 ms, 785 B | 7.671 ms, 4.71 KB | 4.820 ms, 1.61 MB | 24.928 ms, 14.37 MB |
| Cell-by-cell read async | 4.858 ms, 905 B | - | - | - |
| Typed row parsing | 6.113 ms, 3.86 MB | 8.443 ms, 3.87 MB | 12.736 ms, 10.95 MB | 23.252 ms, 14.41 MB |
| Typed row parsing async | 6.187 ms, 3.86 MB | - | - | - |
| Row writing | 6.625 ms, 4.00 MB | 7.083 ms, 4.01 MB | 6.928 ms, 4.04 MB | 14.091 ms, 13.78 MB |
For raw CSV reads, ExcelReader is ~1.5x faster than Sep while allocating ~6x less; Sylvan.Data.Csv is marginally faster here (~9%) but allocates ~2,150x more (1.61 MB vs 785 B) — worth it only if raw wall-clock time matters more than memory pressure; CsvHelper is ~4.8x slower and allocates ~19,200x more. For typed CSV parsing (the more common case — building actual records), ExcelReader is ~1.4x faster than Sep, ~2.1x faster than Sylvan.Data.Csv, and ~3.8x faster than CsvHelper, with the lowest allocation of the group. For CSV writing, ExcelReader, Sep, and Sylvan.Data.Csv are all within ~7% of each other, and ~2.1x faster than CsvHelper; the ~4 MB shown across the first three is primarily the benchmark's pre-sized destination MemoryStream, not per-row writer state.
Real data reads
This benchmark reads a real workbook exported in multiple formats.
| Format | ExcelReader | Sylvan |
|---|---|---|
| XLSX | 66.664 ms, 67.89 KB | 201.999 ms, 648.21 KB |
| XLSM | 66.822 ms, 67.93 KB | 207.699 ms, 648.29 KB |
| XLSB | 22.936 ms, 49.09 KB | 30.097 ms, 338.91 KB |
| XLS | 11.895 ms, 13.31 KB | 18.355 ms, 189.91 KB |
| CSV | 5.952 ms, 785 B | 10.522 ms, 35.75 MB |
On this real-data workload, ExcelReader is ~3.0x faster than Sylvan for XLSX, ~3.1x faster for XLSM, ~1.3x faster for XLSB, ~1.5x faster for XLS, and ~1.8x faster for CSV — allocating ~9.5x less for XLSX/XLSM, ~6.9x less for XLSB, ~14.3x less for XLS, and ~47,750x less for CSV (785 B vs 35.75 MB).
Typed record writing
WorkbookRecordWriter/RecordWriter (the header-plus-one-row-per-object API — see Write typed records) across all four formats, same 50,000-record source:
| Format | Mean | Allocated |
|---|---|---|
| XLSX | 15.410 ms | 4.02 MB |
| XLSB | 8.467 ms | 4.02 MB |
| XLS | 5.365 ms | 4.03 MB |
| CSV | 7.183 ms | 4.00 MB |
Relative ordering matches the lower-level writers above (XLS fastest, then XLSB, then CSV, then XLSX) — the record-mapping layer adds negligible overhead over hand-written cell-by-cell writes.
Ref struct typed parsing (zero-copy)
RefParser.ParseNamed<T> (see Parse into a ref struct) extends ExcelParser<T>'s reflection/attribute-driven column mapping to ref struct targets, binding a ReadOnlySpan<byte> property directly to the cell's raw bytes instead of allocating a string. Same generated XLSX workbook, same 50,000 rows, same four columns — only the target type and binding strategy change:
| Target | Mean | Allocated |
|---|---|---|
class (ExcelParser<T>) |
15.19 ms | 3.88 MB |
struct (ExcelParser<T>) |
15.10 ms | 1.59 MB |
ref struct + span binding (RefParser.ParseNamed<T>) |
12.91 ms | 17.17 KB |
Parsing into a ref struct with a ReadOnlySpan<byte> text column removes essentially all per-row allocation — ~99.6% less than the class baseline — and is ~15% faster, since there's no per-row model allocation and no per-row string allocation for the text column. It is not AOT/trim-safe (reflection-based, same tradeoff as ExcelParser<T>). It can be consumed with foreach or await foreach but not through IEnumerable<T>/IAsyncEnumerable<T>/LINQ — a ref struct element can't be boxed through those interfaces.
Cold start
First use of ExcelParser<T>/RecordWriter in a process pays a one-time reflection + Expression.Compile cost (16 launches, cold JIT, 200 rows):
| Scenario | Mean | Allocated |
|---|---|---|
| First typed parse | 39.46 ms | 75.17 KB |
| First typed record write | 21.11 ms | 89.5 KB |
This cost is paid once per type per process and cached thereafter — irrelevant for long-running services, worth knowing for CLI tools or serverless cold starts.
Run the benchmarks locally:
dotnet run --project tests/ExcelReader.Benchmarks/ExcelReader.Benchmarks.csproj --configuration Release -- --filter *
Install
dotnet add package ExcelReader.NET
Read rows
using ExcelReader.Core.Enums;
using ExcelReader.Core.Reader;
using var reader = Excel.FromFile("report.xlsx");
foreach (var row in reader)
{
string name = row[0].GetString();
if (row[1].TryParse(null, out int quantity))
{
Console.WriteLine($"{name}: {quantity}");
}
if (row[2].Type == CellType.Date && row[2].TryGetDateTime(reader.IsDate1904, out var date))
{
Console.WriteLine(date);
}
}
Open by auto-detecting the format
Excel.Open picks the reader from the file signature (XLSX/XLSB are ZIP packages, XLS is an OLE2 document) and returns an IExcelRowReader. The interface exposes GetEnumerator() directly, so no pattern-match is needed for basic row iteration.
using ExcelReader.Core.Reader;
using IExcelRowReader reader = Excel.Open("report.xlsx"); // or report.xlsb / report.xls
foreach (var row in reader)
{
Console.WriteLine(row[0].GetString());
}
Sheet navigation (SheetCount, SheetName, MoveToSheet(index), TryMoveToSheet(name)) is available on IExcelRowReader itself, so you can walk every sheet without knowing the format:
using IExcelRowReader reader = Excel.Open("report.xlsx");
for (int i = 0; i < reader.SheetCount; i++)
{
reader.MoveToSheet(i);
Console.WriteLine(reader.SheetName);
foreach (var row in reader)
{
Console.WriteLine(row[0].GetString());
}
}
CSV is exposed as a single, unnamed sheet (SheetCount == 1, SheetName == ""). Pattern-match to the concrete type only for reader-specific internals beyond this surface.
OpenAsync is the async counterpart. Both require a seekable stream (or a file path) so the signature can be read without consuming the input.
Read asynchronously
Every reader supports await foreach. For XLSX files, the async reader buffers one row at a time and uses the same row parser as the sync reader, so sync and async reads stay behaviorally aligned while awaits happen only when more bytes are needed.
using ExcelReader.Core.Reader;
await using var reader = await Excel.FromFileAsync("report.xlsx", cancellationToken);
await foreach (var row in reader)
{
Console.WriteLine(row[0].GetString());
}
await foreach binds to the reader's GetAsyncEnumerator() by pattern — the sheet is opened synchronously and only each row advance is awaited. Because Row and Cell are ref struct types, the current row cannot be held across an await inside the loop body: read its cells (or copy the values out) before awaiting anything else.
When you need the sheet opened asynchronously too (e.g. the first read touches the network), or you need to await while a row is in scope, drive the enumerator manually via GetAsyncEnumeratorAsync, which awaits the open and threads the cancellation token:
await using var reader = await Excel.FromFileAsync("report.xlsx", cancellationToken);
await using var rows = await reader.GetAsyncEnumeratorAsync(cancellationToken);
while (await rows.MoveNextAsync())
{
var row = rows.Current;
Console.WriteLine(row[0].GetString());
}
await foreach does not accept .WithCancellation(ct): Row being a ref struct rules out IAsyncEnumerable<Row>, so the loop binds to the pattern rather than the interface. Pass the token at open time (as above), or use the manual GetAsyncEnumeratorAsync(ct) loop.
Parse typed rows
ExcelParser<T> maps worksheet columns to the public settable properties of T. Columns match on the property name, or on [ExcelColumn("header")] aliases — repeat the attribute to accept several headers. The first row is the header by default.
using ExcelReader.Core.Parser;
using ExcelReader.Core.Reader;
public sealed class ChangeRow
{
[ExcelColumn("file")]
public string File { get; set; } = "";
[ExcelColumn("lines_added")]
public int LinesAdded { get; set; }
}
using var reader = Excel.FromFile("changes.xlsx");
var parser = new ExcelParser<ChangeRow>();
foreach (var item in parser.Parse(reader))
{
Console.WriteLine($"{item.File}: +{item.LinesAdded}");
}
Built-in property types: string, bool, DateTime, DateOnly, Guid, every integral and floating type plus decimal, and enums (matched by member name or numeric value). Each also works as a Nullable<T>. Empty cells leave the property at its default; an unparseable cell is skipped (keeps the default) unless the column is required. T needs no parameterless-constructor constraint, so models with required members are supported.
Parse and ParseAsync also accept the IExcelRowReader from Excel.Open, so you can parse without knowing the concrete format:
using IExcelRowReader reader = Excel.Open("changes.xlsx"); // or .xlsb / .xls
foreach (var item in new ExcelParser<ChangeRow>().Parse(reader)) { /* ... */ }
Parser configuration
Pass an ExcelParserConfig to control header handling and culture:
using System.Globalization;
using ExcelReader.Core.Parser;
var config = new ExcelParserConfig
{
HeaderRow = 1, // 1-based row holding the headers
ColumnNameComparer = StringComparer.OrdinalIgnoreCase,
HeaderNormalization = HeaderNormalization.Trim | HeaderNormalization.CollapseSpaces,
Culture = CultureInfo.GetCultureInfo("pt-BR"), // parse "1.234,56" as 1234.56m
};
var parser = new ExcelParser<ChangeRow>(config);
Culture applies when parsing text-backed numeric/Guid cells (XLSX inline and shared strings); binary numeric cells (XLS/XLSB) carry a raw value and ignore it. HeaderNormalization flags (Trim, CollapseSpaces, RemoveDiacritics) are applied to both the sheet headers and the property names before matching.
Required columns
Mark a property [ExcelRequired] to assert its column exists and carries a value:
public sealed class Order
{
[ExcelRequired]
public int Id { get; set; }
[ExcelRequired(AllowEmpty = true)] // column must exist; blank cells allowed
public string? Note { get; set; }
}
- A missing required header throws when the header row is read, listing every missing column.
- By default each data row must have a non-empty cell; the first blank throws, naming the column and row number.
AllowEmpty = truerelaxes this to column presence only. - The check covers presence, not parseability — a present-but-malformed value does not throw here.
Custom converters
For types the built-in parsers do not handle — money strings, custom formats, domain value objects — implement IExcelCellConverter<T> and attach it with [ExcelConverter]. T must be the property's exact type. One instance is created and reused across all rows, so converters must be stateless.
using System.Globalization;
using ExcelReader.Core.Parser;
using ExcelReader.Core.ValueObjects;
public sealed class BrlMoneyConverter : IExcelCellConverter<decimal>
{
public bool TryConvert(in Cell cell, bool isDate1904, IFormatProvider provider, out decimal value)
{
string text = cell.GetString().Replace("R$", "", StringComparison.Ordinal).Trim();
return decimal.TryParse(text, NumberStyles.Currency, CultureInfo.GetCultureInfo("pt-BR"), out value);
}
}
public sealed class Invoice
{
[ExcelConverter(typeof(BrlMoneyConverter))]
public decimal Total { get; set; }
}
Return false to signal a parse failure (the property keeps its default). Empty cells are skipped before the converter runs.
Parse into a ref struct (zero-copy)
RefParser.ParseNamed<T> (.NET 9+) targets a ref struct model instead of a class/struct — same attribute-driven column matching as ExcelParser<T> ([ExcelColumn], [ExcelRequired], [ExcelConverter]), but a ReadOnlySpan<byte> property binds directly to the cell's raw bytes instead of allocating a string:
using System.Text;
using ExcelReader.Core.Parser;
using ExcelReader.Core.Reader;
public readonly ref struct ChangeRowRef
{
public ReadOnlySpan<byte> File { get; init; } // zero-copy — aliases the reader's row buffer
public int LinesAdded { get; init; }
}
using var reader = Excel.FromFile("changes.xlsx");
foreach (ChangeRowRef item in RefParser.ParseNamed<ChangeRowRef>(reader))
{
Console.WriteLine($"{Encoding.UTF8.GetString(item.File)}: +{item.LinesAdded}");
}
The sequence also supports await foreach, so a ref struct model can be parsed asynchronously — the rows are streamed via MoveNextAsync while the model stays a zero-copy ref struct:
await using var reader = await Excel.FromFileAsync("changes.xlsx");
await foreach (ChangeRowRef item in RefParser.ParseNamed<ChangeRowRef>(reader))
{
Console.WriteLine($"{Encoding.UTF8.GetString(item.File)}: +{item.LinesAdded}");
}
A few differences from ExcelParser<T>:
- Span fields alias the reader's row buffer — valid only until the next row. Copy them out (e.g.
Encoding.UTF8.GetString(span)) if you need to keep the value past the loop body. Underawait foreach, the same rule means the model can't be held across anawaitin the loop body. foreach/await foreachonly. Consumption is pattern-based — the sequence cannot be surfaced throughIEnumerable<T>,IAsyncEnumerable<T>, or LINQ, because aref structelement can't be boxed through those interfaces (IAsyncEnumerable<T>in particular forbids aref structelement type — CS9267). Iterate it directly.- Not AOT/trim-safe, same tradeoff as
ExcelParser<T>(both reflect overT's properties and compile setters at runtime). - A regular
struct/classmodel works withParseNamedtoo — only a genuineref structmodel gets the extra zero-copy span-property binding.
Write XLSX workbooks
using ExcelReader.Core.Writer;
await using var stream = File.Create("out.xlsx");
await using var workbook = await XlsxWorkbookWriter.CreateAsync(stream);
await workbook.StartAsync();
await using (var sheet = workbook.AddSheet("Summary"))
{
await sheet.StartAsync();
await using (var row = await sheet.StartRowAsync())
{
row.Write("Name");
row.Write("Total");
row.Write("Created");
}
await using (var row = await sheet.StartRowAsync())
{
row.Write("Q1");
row.Write(42);
row.Write(DateTime.UtcNow);
}
}
await workbook.EndAsync();
By default, the XLSX writer emits inline strings to keep memory usage flat while rows stream out. If your workbook repeats many strings and smaller files matter more than the extra lookup table, opt in to shared strings:
await using var workbook = await XlsxWorkbookWriter.CreateAsync(stream, useSharedStrings: true);
Read and write XLSB workbooks (BIFF12)
Use Excel.FromXlsbFile, Excel.FromXlsb, Excel.FromXlsbFileAsync, or Excel.FromXlsbAsync to open XLSB directly. For writing, use XlsbWorkbookWriter, XlsbSheetWriter, and XlsbRowWriter.
using ExcelReader.Core.Writer;
await using var stream = File.Create("out.xlsb");
await using var workbook = await XlsbWorkbookWriter.CreateAsync(stream);
await workbook.StartAsync();
await using (XlsbSheetWriter sheet = workbook.AddSheet("Summary"))
{
await sheet.StartAsync();
await using (XlsbRowWriter row = await sheet.StartRowAsync())
{
row.Write("Name");
row.Write("Total");
row.Write("Created");
}
}
await workbook.EndAsync();
The XLSB writer also defaults to inline string cells. Pass useSharedStrings: true to deduplicate repeated text into sharedStrings.bin.
Write XLS workbooks (BIFF8)
XlsWorkbookWriter emits a binary BIFF8 .xls file. The sheet and row APIs are synchronous; only the final EndAsync (which assembles and flushes the OLE container) is async. BIFF8 is capped at 65,536 rows × 256 columns per sheet.
using ExcelReader.Core.Writer;
await using var stream = File.Create("out.xls");
await using var workbook = XlsWorkbookWriter.Create(stream);
workbook.Start();
using (var sheet = workbook.AddSheet("Summary"))
{
sheet.Start();
using (var row = sheet.StartRow())
{
row.Write("Name");
row.Write("Total");
row.Write("Created");
}
using (var row = sheet.StartRow())
{
row.Write("Q1");
row.Write(42);
row.Write(DateTime.UtcNow);
}
}
await workbook.EndAsync();
Write typed records
The low-level writers above give you cell-by-cell control. When you just want to dump a collection of objects to a sheet, WorkbookRecordWriter writes a header row followed by one row per record, mapping each public readable property to a column. It is generic over the low-level interfaces, so the same API targets XLSX, XLSB, XLS, and CSV — pick the format with a RecordWriter.Create* factory.
using ExcelReader.Core.Writer;
public sealed class Sale
{
public string? Region { get; set; }
public int Units { get; set; }
public decimal Revenue { get; set; }
public DateOnly Date { get; set; }
}
var sales = new[]
{
new Sale { Region = "North", Units = 42, Revenue = 1234.50m, Date = new DateOnly(2026, 1, 2) },
new Sale { Region = "South", Units = 17, Revenue = 512.00m, Date = new DateOnly(2026, 1, 3) },
};
await using var stream = File.Create("sales.xlsx");
await using var writer = await RecordWriter.CreateXlsxAsync(stream); // or CreateXlsbAsync / CreateXlsAsync / CreateCsv
await writer.WriteSheetAsync("Sales", sales);
Each WriteSheetAsync call targets a new sheet (a duplicate name throws), so one workbook can hold sheets of different record types. RecordWriter.CreateCsv is the exception: a CSV file is a single sheet, so a second WriteSheetAsync throws (the sheet name is ignored). An IAsyncEnumerable<T> overload streams records that are produced asynchronously. The written file round-trips straight back through ExcelParser<T> because the headers are the property names.
Column behavior mirrors the parser attributes:
[ExcelColumn("Header")]— use a custom header instead of the property name (the first alias wins).[ExcelIgnore]— exclude a property from both writing and parsing (for computed/transient members).[ExcelConverter(typeof(MyConverter))]— if the converter also implementsIExcelCellWriter<T>, it controls how the value is written, so a custom type round-trips through the same converter it reads with.
DateTime and DateOnly are written as Excel date serials; TimeOnly as a time-of-day fraction. Numeric properties become number cells; any other type is written as its ToString() text. (CreateCsv follows the CSV rules instead — see Write CSV — writing DateTime/DateOnly as ISO text and TimeOnly as a time-of-day fraction, all still round-tripping through ExcelParser<T>.)
Read CSV
CsvReader streams RFC 4180 CSV (quoted fields, embedded delimiters/newlines, ""-escaped quotes) through the same Row/Cell model as the Excel readers, so ExcelParser<T> works on it unchanged.
using ExcelReader.Core.Parser;
using ExcelReader.Core.Reader;
using var reader = Excel.FromCsvFile("report.csv");
foreach (var row in reader)
{
Console.WriteLine(row[0].GetString());
}
// Typed parsing works exactly like the Excel readers:
foreach (var item in new ExcelParser<ChangeRow>().Parse(reader))
{
Console.WriteLine($"{item.File}: +{item.LinesAdded}");
}
Excel.FromCsv/FromCsvFile/FromCsvAsync/FromCsvFileAsync mirror the other formats' factory shape. Pass CsvReaderOptions to change the delimiter/quote character, supply a non-UTF-8 Encoding (transcoded to UTF-8 internally), or turn off BOM detection:
var options = new CsvReaderOptions { Delimiter = (byte)';' };
using var reader = Excel.FromCsvFile("relatorio.csv", options);
Every CSV cell is text (CellType.ExcelString, or CellType.Empty for a blank field); at the reader level there is no binary numeric or date representation, so Cell.TryGetDateTime/IsDate1904 (always false for CSV) do not apply. The typed parser, however, is CSV-specialized: ExcelParser<T>.Parse(CsvReader) parses DateTime/DateOnly columns directly from the cell text (ISO or culture format, honoring Culture — e.g. pt-BR 02/07/2026), so no [ExcelConverter] is needed for dates. All the usual attributes work unchanged ([ExcelColumn] aliases, [ExcelRequired], [ExcelConverter]), and a converter still takes precedence over the built-in date parsing. (Holding the reader as IExcelRowReader instead routes through the generic Excel pipeline, where dates use serial-number semantics — prefer the concrete Parse(CsvReader) overload for CSV.)
Excel.Open/OpenAsync do not auto-detect CSV — plain text has no magic-byte signature to sniff, so open CSV explicitly via Excel.FromCsv*.
Write CSV
CsvWriter emits RFC 4180 CSV: no sheets, styles, or shared strings, so rows stream straight to the output.
using ExcelReader.Core.Writer;
using var stream = File.Create("out.csv");
using var writer = CsvWriter.Create(stream);
using (CsvRowWriter row = writer.StartRow())
{
row.Write("Name");
row.Write("Total");
row.Write("Created");
}
using (CsvRowWriter row = writer.StartRow())
{
row.Write("Q1");
row.Write(42);
row.Write(DateTime.UtcNow);
}
Fields are quoted only when they contain the delimiter, quote character, \r, or \n; embedded quotes are doubled. bool writes as lowercase true/false and DateTime/DateOnly as round-trip ISO 8601 ("O"); TimeOnly as a time-of-day fraction — all matching what ExcelParser<T>.Parse(CsvReader) expects, so a file written by CsvWriter parses back without configuration. Skip(count) writes empty fields to keep column positions aligned (CSV has no sparse-cell concept). Pass CsvWriterOptions to change the delimiter/quote byte, mirroring CsvReaderOptions.
To dump a collection of typed records instead of writing cells by hand, use RecordWriter.CreateCsv(stream) — the same record-writing API as the Excel formats, restricted to a single sheet.
Notes
- Reads
.xlsx,.xlsb(BIFF12),.xls(BIFF8), and.csv; writes.xlsx,.xlsb,.xls, and.csv. - Reads one sheet at a time (XLSX/XLSB/XLS); use
MoveToSheet(index)orTryMoveToSheet(name)to switch sheets. CSV has no sheets. - Missing cells in sparse rows are exposed as empty cells.
- String conversion allocates only when you call
GetString(). - The XLSX scanner accepts the SpreadsheetML shapes commonly emitted by non-Excel producers, including single-quoted attributes, comments in
sheetData, and CDATA text runs. - Readers bound untrusted input by default: 512 MB total decompressed ZIP data, 32 MB per cell/row value buffer, and 128 MB for shared strings. Pass
ExcelReaderOptionsto theExcel.From*/Excel.Open*factories to tune these limits; set a limit to0to opt out and restore unlimited behavior for that limit.CsvReaderhas its ownCsvReaderOptions.MaxCellBytes(default 32 MB) for the same purpose. - The XLSX writer emits a compact workbook with strings, numbers, booleans, dates, and blank cells; shared strings are opt-in.
- The XLSB writer emits BIFF12 workbook parts inside the standard XLSB ZIP package; shared strings are opt-in.
- The XLS writer buffers records in memory and assembles the OLE container at
EndAsync; choose it when write throughput matters more than peak allocation.
Build
dotnet restore ExcelReader.slnx
dotnet build ExcelReader.slnx --configuration Release
dotnet test --project tests/ExcelReader.Tests/ExcelReader.Tests.csproj --configuration Release
License
ExcelReader is licensed under the MIT License. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ExcelReader.NET:
| Package | Downloads |
|---|---|
|
ExcelReader.Arrow
Apache Arrow RecordBatch conversion for ExcelReader.NET. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.1.0 | 0 | 9/21/2026 |
| 4.0.0 | 46 | 9/20/2026 |
| 3.1.0 | 115 | 9/14/2026 |
| 3.0.2 | 117 | 9/9/2026 |
| 3.0.1 | 3,184 | 9/4/2026 |
| 3.0.0 | 116 | 9/4/2026 |
| 2.3.0 | 1,802 | 8/30/2026 |
| 2.2.0 | 109 | 8/26/2026 |
| 2.1.3 | 141 | 8/20/2026 |
| 2.1.2 | 2,904 | 8/16/2026 |
| 2.1.1 | 100 | 8/14/2026 |
| 2.1.0 | 104 | 8/14/2026 |
| 2.0.1 | 108 | 8/5/2026 |
| 2.0.0 | 172 | 8/5/2026 |
| 1.4.5 | 118 | 8/4/2026 |
| 1.4.4 | 121 | 7/31/2026 |
| 1.4.2 | 146 | 7/28/2026 |
| 1.4.1 | 134 | 7/23/2026 |
| 1.4.0 | 106 | 7/22/2026 |
| 1.3.1 | 115 | 7/21/2026 |
See the changelog: https://github.com/GabrielMarquezMatte/ExcelReader/releases