Fast, typed, streaming read and write of tabular data in xlsx format. Nothing else.
Requires .NET 10+.
var handler = Xlsx.CreateHandler();
await handler.WriteAsync(stream, rows);
foreach (var row in handler.Read(stream))
{
// each cell is a FieldValue: Text, Number, Date, Boolean, or Empty
}rows |> Xlsx.writeAsync stream |> _.Wait()
Xlsx.read stream
|> Seq.iter handleDataRowEach cell is a FieldValue — five types: Text, Number, Date, Boolean, Empty.
See C# examples for manual mapping, typed object writing, and reading with pattern matching.
See F# examples for idiomatic writing and reading with pattern matching.
The F# package also includes optional support for FSharpOrDi — a functional dependency injection library where function signatures drive the wiring. Instead of manually passing dependencies, you declare what a function needs through its type signature and FSharpOrDi resolves the rest. See the DI example for a working demonstration.
| What you need | StreamScheme |
|---|---|
| Read tabular xlsx data, row by row | Yes |
| Write tabular xlsx data, row by row | Yes |
| Typed cells (text, numbers, dates, booleans) | Yes |
| Low memory, streaming — no full-file buffering | Yes |
| Roundtrip: write then read back identically | Yes |
| Column widths | Yes |
| Cell formatting, fonts, colors | No |
| Merged cells | No |
| Formulas | No |
| Charts or images | No |
| Multiple sheets | No |
| Row heights | No |
| Headers, footers, print settings | No |
| Password protection | No |
If you need presentation, use a full Excel library.
C#:
dotnet add package StreamSchemeF#:
dotnet add package StreamScheme.FSharpStreamScheme lets you control how repeated text values are stored in the xlsx output:
- Off — inline every string. Fastest, no overhead. Best when values are mostly unique.
- Always — deduplicate all strings via a shared strings table. Smaller files when few distinct values repeat across many cells.
- Windowed(n) — deduplicate within a sliding window of n rows. Bounded memory, good for mixed data where some columns repeat and others don't.
100,000 rows. Ratios are relative to StreamScheme (baseline = 1.00).
Writing — unique strings (10 columns, XML-escapable characters)
No repeated data — shared strings cannot help here.
| Method | Speed Ratio | Allocated | Alloc Ratio | Output Size | Size Diff |
|---|---|---|---|---|---|
| StreamScheme Off | 1.00 | 27.47 MB | 1.00 | 3.18 MB | |
| SpreadCheetah | 1.11 | 31.29 MB | 1.14 | 3.18 MB | |
| MiniExcel | 4.98 | 607.06 MB | 22.10 | 3.88 MB | +22% |
Writing — sparse categories (20 columns, 70% empty, verbose status strings)
Few distinct values repeated across many cells — shared strings deduplicate effectively.
| Method | Speed Ratio | Allocated | Alloc Ratio | Output Size | Size Diff |
|---|---|---|---|---|---|
| StreamScheme Off | 1.00 | 22.9 MB | 1.00 | 3.13 MB | |
| StreamScheme Always | 0.98 | 22.9 MB | 1.00 | 2.60 MB | -17% |
| StreamScheme Reflection Off | 1.28 | 38.24 MB | 1.67 | 3.13 MB | |
| StreamScheme Reflection Always | 1.25 | 38.24 MB | 1.67 | 2.60 MB | -17% |
| SpreadCheetah | 1.23 | 48.07 MB | 2.10 | 3.13 MB | |
| MiniExcel | 5.63 | 425.44 MB | 18.58 | 8.79 MB | +181% |
Writing — mixed data (5 unique strings + 5 repeated category enums)
Half unique, half repeated — windowed shared strings reduces file size for the repeated columns.
| Method | Speed Ratio | Allocated | Alloc Ratio | Output Size | Size Diff |
|---|---|---|---|---|---|
| StreamScheme Off | 1.00 | 24.42 MB | 1.00 | 1.99 MB | |
| StreamScheme Windowed | 1.63 | 85.77 MB | 3.51 | 1.85 MB | -7% |
| StreamScheme Reflection Off | 1.15 | 35.87 MB | 1.47 | 1.99 MB | |
| StreamScheme Reflection Windowed | 1.79 | 97.22 MB | 3.98 | 1.85 MB | -7% |
| SpreadCheetah | 1.14 | 25.19 MB | 1.03 | 1.99 MB | |
| MiniExcel | 5.37 | 363.65 MB | 14.89 | 5.06 MB | +154% |
Reading — mixed types (6 columns, 2.04 MB file)
| Method | Speed Ratio | Allocated | Alloc Ratio |
|---|---|---|---|
| StreamScheme | 1.00 | 82.83 MB | 1.00 |
| MiniExcel | 2.88 | 585.69 MB | 7.07 |
Allocation numbers reflect the mapping layer, not total memory — the source data must live somewhere regardless. Both benchmarks allocate a new array per row. SpreadCheetah supports an imperative API where a single DataCell[] is reused across rows, which would reduce mapping layer allocation to near zero.
StreamScheme allocates FieldValue records per cell — short-lived Gen0 objects, collected quickly. This is a deliberate tradeoff: the IEnumerable<IEnumerable<FieldValue>> API is streaming and composable (LINQ-friendly) at the cost of Gen0 churn.
StreamScheme's date format detection code is adapted from MiniExcel (Apache 2.0), which credits ExcelNumberFormat (MIT) by andersnm.
SpreadCheetah (MIT) served as inspiration and the primary performance comparison baseline.
Built by Immersus Machina