SheetStream.NET
0.4.0
dotnet add package SheetStream.NET --version 0.4.0
NuGet\Install-Package SheetStream.NET -Version 0.4.0
<PackageReference Include="SheetStream.NET" Version="0.4.0" />
<PackageVersion Include="SheetStream.NET" Version="0.4.0" />
<PackageReference Include="SheetStream.NET" />
paket add SheetStream.NET --version 0.4.0
#r "nuget: SheetStream.NET, 0.4.0"
#:package SheetStream.NET@0.4.0
#addin nuget:?package=SheetStream.NET&version=0.4.0
#tool nuget:?package=SheetStream.NET&version=0.4.0
๐ SheetStream.NET
A fast, zero-dependency, MIT-licensed .NET library for creating, filling, editing and reading Excel (.xlsx) files. (Not related to the Node.js package of the same name.)
SheetStream streams rows straight into the file instead of building the workbook in memory, so large exports stay fast and memory stays flat. It was built at P4 Software when EPPlus moved to a commercial licence, and it covers what business reports actually use: styles, tables, charts, conditional formatting, validation, templates designed in Excel, and a streaming reader for imports.
โจ Why SheetStream?
- โ๏ธ MIT licensed: free for commercial use with no licence keys or seat counts.
- ๐ฆ Zero Dependencies: Built entirely on native .NET
System.IO.CompressionandSystem.XmlAPIs. Targetsnetstandard2.0andnet8.0, so it runs on .NET Framework 4.7.2+ as well as modern .NET. - โก Streaming with flat memory: rows go straight through an
XmlWriterinto the zip archive. In the benchmarks SheetStream allocates about 13ร less than the next library, and 10 million cells write in about 5 seconds with a 41 MB peak working set, whether the sheet is long or wide. - ๐งฐ The features real reports use: tables, charts, pictures, conditional formatting, data validation, hyperlinks, comments, rich text, protection, print setup, outlining, AutoFit, template filling, async loading, and a streaming reader for imports.
- ๐ ASP.NET Core ready: the optional
SheetStream.AspNetCorepackage returns a streamed Excel download in one line. - โ Verified output: every feature is validated against the ECMA-376 schema with the Open XML SDK validator in CI, and showcase workbooks are opened in real Excel before each release. The reader is fuzz-tested against damaged files.
๐ฆ Installation
dotnet add package SheetStream.NET
dotnet add package SheetStream.AspNetCore # optional: one-line Excel downloads
Upgrading from the 0.3.x SheetStream package: dotnet remove package SheetStream, then add SheetStream.NET. The namespace is still SheetStream, so no code changes.
Full documentation and API reference: https://p4software.github.io/SheetStream.NET/
Try it in your browser without installing anything: DotNetFiddle: write a styled Excel file and read it back.
๐ป Quick Start
using SheetStream;
using SheetStream.Styles;
using (var package = ExcelPackage.Create("FinancialReport.xlsx"))
{
var workbook = package.Workbook;
var sheet = workbook.Worksheets.Add("Q1 Report");
sheet.Column(1).SetWidth(10);
sheet.Column(2).SetWidth(28);
sheet.Column(3).SetWidth(18);
sheet.FreezePanes(rows: 1);
var headerStyle = workbook.Styles.Create(o =>
{
o.Font.Bold = true;
o.Font.Color = "#FFFFFF";
o.Fill.BackgroundColor = "#4F81BD";
o.Border.Style = BorderStyle.Thin;
});
var currencyStyle = workbook.Styles.Create(o => o.NumberFormat = "$#,##0.00");
sheet.Row(1)
.WriteCell("ID", headerStyle)
.WriteCell("Product Name", headerStyle)
.WriteCell("Revenue (USD)", headerStyle);
for (int i = 1; i <= 100; i++)
{
sheet.Row(i + 1)
.WriteCell(i)
.WriteCell($"Enterprise SKU-{i}") // Shared string (auto-deduplicated)
.WriteCell(1500.50 * i, currencyStyle);
}
sheet.Row(102).WriteCell("Total", headerStyle).WriteEmpty(headerStyle).WriteFormula("SUM(C2:C101)", currencyStyle);
package.Save(); // Also happens automatically when the package is disposed
}
Need bytes for a web download, like EPPlus' GetAsByteArray()?
byte[] bytes = ExcelPackage.CreateBytes(wb => wb.Worksheets.Add("Export").LoadFromCollection(orders));
byte[] bytes = await ExcelPackage.CreateBytesAsync(async wb => // Await a query per sheet
wb.Worksheets.Add("Export").LoadFromCollection(await db.Orders.ToListAsync()));
๐งญ Feature tour
Load data in one call
var result = sheet.LoadFromCollection(orders, new LoadOptions
{
HeaderStyle = headerStyle,
TableName = "Orders", // Becomes an Excel table with filter buttons and banding
TableStyle = "TableStyleMedium2",
});
sheet.LoadFromDataTable(dataTable);
sheet.LoadFromDataReader(sqlCommand.ExecuteReader()); // Streams a large query row by row
sheet.LoadFromArrays(listOfObjectArrays); // First array is the header row (EPPlus LoadFromArrays)
Control columns with attributes: [SheetColumn("Invoice #", Order = 0, NumberFormat = "$#,##0.00")], [SheetColumn(Ignore = true)], or [DisplayName("...")]. IDictionary<string, object> rows (ExpandoObject, Dapper) work too.
Tables, conditional formatting and validation
var table = sheet.AddTable("Sales", "A1:D101"); // Add before writing the header row
table.StyleName = "TableStyleMedium9";
var bad = workbook.Styles.CreateDifferential(o => { o.Font.Color = "#9C0006"; o.Fill.BackgroundColor = "#FFC7CE"; });
sheet.AddConditionalFormatting("D2:D101")
.CellValue(ConditionalOperator.LessThan, "0", bad)
.DataBar("#638EC6");
sheet.AddConditionalFormatting("E2:E101").IconSet(IconSetType.ThreeTrafficLights);
sheet.AddDataValidation("C2:C101").List("Open", "Closed", "On hold").WithError("Invalid", "Pick from the list.");
sheet.AddDataValidation("F2:F101").WholeNumber(ValidationOperator.Between, 1, 100);
Conditional formatting covers cell value, expression, contains text, duplicates, unique, top / bottom N, above / below average, two- and three-colour scales, data bars and icon sets. Validation covers lists (inline or from a range), whole numbers, decimals, dates, text length and custom formulas.
Charts and pictures
var chart = sheet.AddChart(ChartType.Column, "H2:P20");
chart.Title = "Revenue by month";
chart.YAxisTitle = "USD";
chart.ValueAxisNumberFormat = "$#,##0";
chart.AddSeries(values: "C2:C13", categories: "A2:A13", name: "=C1", color: "#4F81BD");
sheet.AddPicture("logo.png", row: 1, column: 8, widthPixels: 160);
Chart types: column, bar (clustered, stacked, 100%), line, line with markers, stacked line, area, stacked area, pie, doughnut, scatter and scatter with lines. Pictures: PNG, JPEG and GIF.
Pivot tables
var loaded = data.LoadFromCollection(orders, new LoadOptions { TableName = "Orders" });
var pivot = workbook.Worksheets.Add("Pivot").AddPivotTable("SalesByRegion", loaded.Table!, row: 3, column: 1);
pivot.AddRowField("Region");
pivot.AddColumnField("Category");
pivot.AddDataField("Amount", PivotFunction.Sum, numberFormat: "$#,##0.00");
pivot.AddDataField("OrderId", PivotFunction.Count, displayName: "Orders");
The pivot sits over a table or range written earlier (AddPivotTable("P", "Data", loaded.Range, 3, 1, loaded.Headers) for a plain range) and is marked refresh-on-open, so Excel computes it when the file opens; the output is checked against real Excel in tests/excel-check-pivot.ps1. Viewers without a pivot engine show only the headers until Excel has opened and saved the file. Calculated fields, grouping and slicers are not supported. See Pivot tables.
Links, comments, rich text and formulas
sheet.Row()
.WriteHyperlink("Open the portal", "https://example.com/orders/42")
.WriteInternalLink("Go to summary", "'Summary'!A1")
.WriteRichText("Status: ", new RichText("LATE") { Bold = true, Color = "#C00000" })
.WriteFormula("B2*TaxRate", cachedValue: 7.0); // Cached result for viewers that do not calculate
sheet.AddComment(2, 3, "Checked by finance", author: "Glenn");
workbook.DefinedNames.Add("TaxRate", "'Settings'!B2");
Formula results for viewers that do not calculate
sheet.CacheFormulaResults(); // Before the first row; about 8 bytes per numeric cell
sheet.Row().WriteCell("Total").WriteFormula("SUM(C2:C101)"); // Written with its result
sheet.Row().WriteCell("Avg").WriteFormula("ROUND(AVERAGE(C2:C101), 2)");
File previews, Quick Look, some mobile viewers and ExcelReader show formula cells blank unless the file carries a cached result. Opt in per sheet and SheetStream evaluates + - * / %, SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, ROUND and ABS over cells already written on that sheet. Anything else (other sheets, IF, forward references, names) is written as before, with no value and Excel calculating on open: never a wrong value.
Layout, printing and protection
sheet.AutoFitColumns(); // Widths measured like Excel's own AutoFit, at save time
sheet.AutoFitColumns((text, bold) => paint.MeasureText(text) / digitWidth); // Or plug in real font metrics
sheet.TabColor = "#C00000";
sheet.ShowGridLines = false;
sheet.Columns(5, 8).SetOutlineLevel(1); // Collapsible column group
sheet.Row(10, new RowOptions { OutlineLevel = 1, Hidden = true });
sheet.PageSetup.Orientation = PageOrientation.Landscape;
sheet.PageSetup.FitToWidth = 1;
sheet.PageSetup.PrintTitleRows = (1, 1);
sheet.PageSetup.Footer = "Page &P of &N";
var input = workbook.Styles.Create(o => o.Protection.Locked = false);
sheet.Protect("password", o => o.AllowSort = true); // Salted SHA-512, like Excel 2013+
workbook.Protect("password"); // Lock the sheet structure
sheet.Visibility = SheetVisibility.Hidden;
AutoFitColumns() sizes columns the way Excel's AutoFit does: each cell is measured as displayed (formatted numbers, dates, bold, font size) with a Calibri 11 glyph table calibrated against Excel, so "Description" comes out 11.2 wide and ten digits 11.1. For another default font or non-Latin text, pass a measurer (sheet.AutoFitColumns((text, bold) => ...) or PackageOptions.TextMeasurer) that returns widths in Excel's column unit: one digit of the default font, so "0000000000" must measure 10.
Fill templates designed in Excel
using var package = ExcelPackage.OpenTemplate("invoice-template.xlsx", "invoice-1042.xlsx");
var sheet = package.Workbook.Worksheets.Open("Lines");
var money = sheet.TemplateStyle("D5"); // Reuse the designer's formatting
var row = 5;
foreach (var line in lines)
sheet.Row(row++).WriteCell(line.Sku).WriteCell(line.Description).WriteCell(line.Qty).WriteCell(line.Amount, money);
package.Save();
Logos, header styles, formulas, charts and print settings come from the template. Tables, filters, validations and conditional formats that start on the first data row stretch over the new rows, and formulas like =SUM(Lines[Amount]) are recalculated when the file opens. See Filling templates.
Edit a small existing workbook
using var package = ExcelPackage.Edit("budget.xlsx"); // In place; or Edit(input, output)
var sheet = package.Workbook.Worksheets.Edit("Summary");
var current = sheet.Cells["B4"].Value; // Typed: double, DateTime, string, bool...
sheet.Cells["B4"].Value = 1400; // Keeps the cell's number format
sheet.Cells["C4"].Formula = "B4*1.21";
var next = sheet.Cells.Dimension.LastRow + 1;
sheet.Cells[next, 1].Value = "Added by code";
package.Save(); // Atomic replace; the original is untouched if saving fails
Untouched cells, rows, charts, pictures, tables and other sheets are written back exactly as they were, and formulas recalculate on open. This is meant for small files (default limit 200,000 cells per sheet, PackageOptions.MaxEditableCells); large files keep using streaming or templates. See Editing existing workbooks.
Async and ASP.NET Core
// Stream an EF Core query into a sheet without materialising it (.NET 8+)
await sheet.LoadFromAsyncEnumerable(db.Orders.AsNoTracking().AsAsyncEnumerable(), new LoadOptions { TableName = "Orders" }, ct);
// Minimal API download (SheetStream.AspNetCore)
app.MapGet("/orders.xlsx", (AppDb db) => ExcelResults.Excel("orders.xlsx", async (wb, ct) =>
await wb.Worksheets.Add("Orders").LoadFromAsyncEnumerable(db.Orders.AsAsyncEnumerable(), null, ct)));
The download is built into a temporary file and copied to the response asynchronously: flat memory, an accurate Content-Length, and no synchronous I/O on the response.
Trimming and Native AOT
[SheetRow] // The bundled source generator writes the column mapping at compile time
public partial class Order
{
[SheetColumn("Invoice #", Order = 0)] public int Id { get; set; }
[SheetColumn(NumberFormat = "$#,##0.00")] public decimal Total { get; set; }
}
sheet.LoadFromCollection(orders); // Uses Order.SheetMapper automatically on .NET 5+
sheet.LoadFromCollection(orders, Order.SheetMapper); // Or pass it explicitly
The net8.0 build is IsTrimmable and IsAotCompatible. Types marked [SheetRow] partial get a generated mapper, so LoadFromCollection<T>, LoadFromAsyncEnumerable<T> and ExcelReader.Read<T> need no reflection and no Expression.Compile; [SheetColumn] and [DisplayName] work exactly as before, and reflection remains the fallback for unmarked types. See Trimming, Native AOT and the source generator.
Random access, EPPlus style
sheet.Cells[3, 2].Value = 42; // Any order
sheet.Cells["B4"].Formula = "SUM(B1:B3)";
sheet.Cells["B4"].Style = boldStyle;
Random-access mode keeps the sheet's cells in memory until it is saved, which is convenient for small, irregular layouts. Use Row() streaming for big exports. A sheet uses one mode or the other.
Reading workbooks
using SheetStream.Reading;
using var reader = ExcelReader.Open("import.xlsx");
foreach (var row in reader.ReadRows("Products")) // Streams; constant memory
Console.WriteLine($"{row.GetString(0)}: {row.GetDouble(2)}");
List<Product> products = reader.Read<Product>("Products").ToList(); // Header row maps to properties
DataTable table = reader.ReadDataTable("Products");
Dates come back as DateTime (built-in and custom date formats, 1904 date system included), elapsed times as TimeSpan, errors as ExcelErrorValue, and formula cells as their cached result.
foreach (var merge in reader.ReadMergedRanges("Report")) // e.g. A1:D1
Console.WriteLine($"{merge.Range} covers B1: {merge.Contains(1, 2)}");
foreach (var link in reader.ReadHyperlinks("Report")) // Url for web/mail links, Location for in-workbook links
Console.WriteLine($"{link.Address}: {link.Url ?? link.Location} ({link.Tooltip})");
Merged ranges and hyperlinks are read from the sheet's trailer only, so the rows are skipped and the call is instant even on a million-row sheet.
// Async reading (.NET 8+): buffer an upload without blocking, then stream rows or objects.
using var reader = await ExcelReader.OpenAsync(Request.Body, cancellationToken: ct); // Non-seekable: copied async, >64 MB spills to a temp file
await foreach (var row in reader.ReadRowsAsync("Products", ct)) // Cancellation checked between rows
Console.WriteLine($"{row.GetString(0)} = {row.GetDouble(2)}");
await foreach (var p in reader.ReadAsync<Product>("Products", ct)) db.Products.Add(p); // Or ReadAsync(sheet, Product.SheetMapper, ct) for AOT
var table = await reader.ReadDataTableAsync("Products", firstRowIsHeader: true, ct);
Design-time analyzer
The package ships a Roslyn analyzer, so the IDE warns before you run: SS0001 layout (FreezePanes, Column, TabColor...) set after rows were written, SS0002 Row() and Cells[] mixed on one sheet, SS0003 rows written out of order. It only reports what it can prove inside one method, so a warning means the code will throw. Nothing to install, nothing added to your output. Details, before/after snippets and how to suppress: docs/analyzers.md.
Web sample
samples/SheetStream.Sample.Web is a minimal API that streams a 50,000-row styled export (GET /orders.xlsx?rows=N) and a Cells[...] summary with charts (GET /report.xlsx) through ExcelResults.Excel. Double-click run.cmd (or dotnet run --project samples/SheetStream.Sample.Web --urls http://localhost:5178) and open http://localhost:5178. Its README has the six lines to copy into your own app and how to call the same endpoint from a Blazor button.
๐ API overview
| Area | What you get |
|---|---|
ExcelPackage |
Create(path), new ExcelPackage(stream), CreateBytes(build), Save(), Dispose() |
Workbook |
Worksheets.Add, Styles, DefinedNames, Properties, Protect |
Worksheet |
Row(), Cells[r, c], Column / Columns, AutoFitColumns, FreezePanes, AutoFilter, MergeCells, AddTable, AddChart, AddPicture, AddPivotTable, AddConditionalFormatting, AddDataValidation, AddHyperlink, AddComment, CacheFormulaResults, Protect, PageSetup, TabColor, Visibility, LoadFromCollection / DataTable / DataReader / AsyncEnumerable |
RowWriter |
WriteCell (string, numbers, bool, dates, TimeSpan, object), WriteCells, WriteFormula (with or without cached value), WriteRichText, WriteHyperlink, WriteInternalLink, WriteInlineString, WriteEmpty, Skip |
StyleSheet |
Create (font, fill, border, number format, alignment, wrap, indent, rotation, shrink, protection), CreateDifferential, Hyperlink |
ExcelReader |
Open / OpenAsync, SheetNames, ReadRows / ReadRowsAsync, Read<T> / ReadAsync<T>, ReadDataTable / ReadDataTableAsync, ReadMergedRanges, ReadHyperlinks |
| Templates | ExcelPackage.OpenTemplate(template, output), Worksheets.Open(name), Worksheet.TemplateStyle(cell), ReplacedTemplateRows |
| Editing | ExcelPackage.Edit(path) / Edit(input, output), Worksheets.Edit(name), Cells.Existing, Cells.Dimension, ExcelCell.Clear() |
SheetStream.AspNetCore |
ExcelResults.Excel(fileName, build), Results.Extensions.Excel(...), ExcelResult |
โ๏ธ How it compares
| EPPlus 8 | ClosedXML | MiniExcel | SheetStream | |
|---|---|---|---|---|
| Licence | Commercial for business use | MIT | Apache 2.0 | MIT |
| Package dependencies | Several | Several | None | None |
| Write model | In memory | In memory | Streaming | Streaming |
| Styles, tables, conditional formatting, validation | โ | โ | Limited | โ |
| Charts and pictures | โ | Pictures only | โ | โ (common chart types) |
| Fill a template designed in Excel | โ | โ | โ | โ (streaming) |
| Edit and re-save an existing workbook | โ | โ | โ | โ (small workbooks, in memory) |
| Read workbooks | โ | โ | โ streaming | โ streaming |
| Formula calculation engine | โ | โ | โ | โ (Excel calculates on open) |
| Pivot tables | โ | โ | โ | โ (refreshed on open) |
Benchmarks
Writing 100,000 rows ร 10 mixed, formatted columns (BenchmarkDotNet, .NET 8, Intel i7-8700, Windows 11):
| Library | Time | Allocated | Peak memory above baseline | File size |
|---|---|---|---|---|
| SheetStream (default compression) | 1,013 ms | 27 MB | 30 MB | 5.7 MB |
SheetStream (CompressionLevel.Fastest) |
534 ms | 27 MB | 31 MB | 6.8 MB |
| MiniExcel 1.46 | 596 ms | 343 MB | 37 MB | 7.2 MB |
| NPOI 2.7.6 SXSSF | 2,040 ms | 704 MB | 65 MB | 5.4 MB |
| EPPlus 4.5.3.3 | 2,287 ms | 390 MB | 184 MB | 5.5 MB |
| ClosedXML 0.105 | 3,103 ms | 1,104 MB | 231 MB | 5.9 MB |
| NPOI 2.7.6 XSSF | 4,658 ms | 1,186 MB | 545 MB | 5.2 MB |
What the numbers say, including where SheetStream does not win:
- Memory is SheetStream's clearest advantage. It allocates about 13 times less than the next library and keeps the lowest peak in both the long and the wide (1,000 ร 1,000) scenarios.
- MiniExcel is faster than SheetStream's default. SheetStream defaults to the smallest file (
CompressionLevel.Optimal). WithFastestcompression SheetStream is quickest in both scenarios, though on the long sheet the lead over MiniExcel is within the margin of error. - NPOI and EPPlus produce slightly smaller files on the long sheet.
- Reading the same 100,000-row file: SheetStream 0.53 s and 119 MB allocated; ExcelDataReader 0.92 s and 211 MB; EPPlus 4.5.3.3 1.2 s and 787 MB; MiniExcel 1.8 s and 2.4 GB; ClosedXML 3.0 s and 1.6 GB.
- At a million rows (1,000,000 ร 10) SheetStream writes in 4.8 s with
Fastestcompression (12.8 s with the default, smaller-file setting) and allocates 275 MB with a 200 MB peak working set; MiniExcel takes 5.3 s and 3.3 GB, EPPlus 4.5.3.3 26 s and 3.9 GB with a 1.5 GB peak, ClosedXML 37 s and 11.6 GB with a 2.4 GB peak. A 1,000 ร 10,000 sheet costs SheetStream the same as a 10,000 ร 1,000 one. - MiniExcel cannot bold the header, so its file carries slightly less formatting.
Full results, the wide-sheet scenario, methodology, machine details and how to rerun everything are in benchmarks/README.md. EPPlus is pinned to 4.5.3.3 and NPOI to 2.7.6, their last releases under plain open-source licences. Numbers come from a developer workstation, so expect some noise; rerun them on your own hardware.
When to choose something else
- You need to edit large workbooks in place. SheetStream edits small workbooks (
ExcelPackage.Edit) by holding the edited sheet in memory; for big files it fills templates or streams. Use ClosedXML (MIT) or EPPlus when you must rewrite a large arbitrary workbook. - You need formulas evaluated in .NET, for example to read a calculated total without Excel. Use ClosedXML or EPPlus, which have calculation engines.
- You need sparklines, macros, encryption, or pivot tables with calculated fields, grouping or slicers. These are not supported.
- You only map simple objects to and from sheets and want the smallest API. MiniExcel is excellent at that.
SheetStream is the right fit when you write large or styled exports, fill branded templates, stream reports from ASP.NET Core, or read big imports with flat memory, under a licence you never have to think about.
Migrating from EPPlus
The full guide, with a real exporter converted line by line, is in docs/migrating-from-epplus.md.
| EPPlus | SheetStream |
|---|---|
new ExcelPackage() + package.SaveAs(file) |
ExcelPackage.Create(path) + package.Save() |
package.GetAsByteArray() |
ExcelPackage.CreateBytes(wb => ...) |
ws.Cells[r, c].Value = x |
ws.Cells[r, c].Value = x, or stream with ws.Row(r).WriteCell(x) |
cell.Style.Font.Bold = true |
Styles.Create(o => o.Font.Bold = true) once, pass the style to WriteCell |
cell.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(79,129,189)) |
o.Fill.BackgroundColor = Colors.Rgb(79, 129, 189) |
cell.Style.Numberformat.Format = "#,##0.00" |
o.NumberFormat = NumberFormats.Decimal2 |
ws.Column(c).Width = 15 |
ws.Column(c).SetWidth(15) |
ws.Cells[...].AutoFitColumns() |
ws.AutoFitColumns() before writing |
ws.Cells["A1"].LoadFromCollection(list, true, TableStyles.Medium2) |
ws.LoadFromCollection(list, new LoadOptions { TableName = "T" }) |
ws.Tables.Add(range, "Sales") |
ws.AddTable("Sales", "A1:D101") |
ws.Drawings.AddChart("c", eChartType.ColumnClustered) |
ws.AddChart(ChartType.Column, "H2:P20") |
ws.Cells["A1"].Hyperlink = new Uri(url) |
row.WriteHyperlink(text, url) |
new ExcelPackage(file).Workbook.Worksheets[0].Cells[r, c].Value |
ExcelReader.Open(file).ReadRows(0) |
Streaming rules
SheetStream writes directly into the zip archive, which is what keeps memory flat. Each rule is enforced with a clear exception:
- One sheet at a time. Adding a worksheet seals the previous one.
- Rows ascend.
Row(n)must be the current row or a later one. Gaps are fine. UseCells[r, c]on a sheet that needs random access. - Layout first. Column widths, frozen panes, tab colour, gridlines and fit-to-page live before the data in the file, so set them before the first row.
AutoFitColumns()andCells[...]defer the header, which lifts this rule for that sheet. - Dispose saves. Leaving a
usingblock without callingSave()still finishes the file. A save that fails is not retried byDispose, so the original exception is the one you see.
๐ ๏ธ Architecture
- Package (
ExcelPackage): owns theZipArchive, streams each worksheet part as it is written, and on save emits the content types, relationships, workbook, styles, shared strings and document properties. - Streaming writer (
Worksheet/RowWriter): rows go through anXmlWriterinto the compressed stream. Column letters are cached and the row number is formatted once per row, so writing a cell allocates nothing. Tables, drawings, charts and comments are separate parts written right after their sheet. - Shared strings (
SharedStringTable): de-duplicates text across all sheets, as Excel does. - Styles (
StyleSheet): de-duplicates fonts, fills, borders, number formats, cell formats and differential formats, written in the exact element order Excel demands. - Reader (
ExcelReader): anXmlReaderpass over each sheet that resolves shared strings and date styles, with DTD processing disabled for untrusted files.
๐งช Building and testing
dotnet build
dotnet test
dotnet run --project samples/SheetStream.Sample # FinancialReport.xlsx
dotnet run --project samples/SheetStream.Sample -- showcase # Showcase.xlsx: every feature
dotnet run -c Release --project samples/SheetStream.Sample -- 1000000 # 1M-row stress run
dotnet run -c Release --project samples/SheetStream.Sample -- 5000 2000 # 10M-cell wide stress run
dotnet run -c Release --project benchmarks/SheetStream.Benchmarks # comparison benchmarks
dotnet tool restore && dotnet docfx docs/docfx.json --serve # documentation site on localhost
๐ข Releasing
Bump <Version> in src/SheetStream/SheetStream.csproj, add a CHANGELOG.md entry, commit, then:
git tag v0.2.0
git push --tags
The Release workflow refuses to run if the tag and the csproj version disagree. It publishes through nuget.org Trusted Publishing, so no API key is stored in the repository.
๐ค Contributing
Contributions are welcome. See CONTRIBUTING.md, the good first issues, and Discussions for questions. Security reports go through SECURITY.md.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on SheetStream.NET:
| Package | Downloads |
|---|---|
|
SheetStream.AspNetCore
ASP.NET Core helpers for SheetStream: return a streamed .xlsx download from a minimal API or controller in one line, with flat memory and no synchronous I/O. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.4.0 | 116 | 10/14/2026 |