Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions .github/workflows/codeql.yml

This file was deleted.

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ All notable changes to SheetStream. Versions follow [Semantic Versioning](https:
- **Edit small existing workbooks**: `ExcelPackage.Edit(path)` (in place, atomic replace) or `Edit(input, output)`, then `Worksheets.Edit(name)` loads the sheet into `Cells[...]`: read values, change cells, add rows, `Cells.Existing`, `Cells.Dimension`, `ExcelCell.Clear()`. Untouched cells, rows, formulas (shared and array formulas included), charts, tables and other sheets are written back unchanged. Guarded by `PackageOptions.MaxEditableCells` (200,000 by default); large files keep using streaming or templates.
- **Trimming and Native AOT**: the `net8.0` build is `IsTrimmable` and `IsAotCompatible`. A bundled **source generator** turns `[SheetRow] partial` types into compile-time mappers (`ISheetRowMapper<T>`, `SheetMappers`), so `LoadFromCollection<T>`, `LoadFromAsyncEnumerable<T>` and `ExcelReader.Read<T>` work without reflection or `Expression.Compile`; new overloads take a mapper explicitly. Reflection stays the fallback for unmarked types and is now annotated for the trimmer.
- **Roslyn analyzer** shipped inside the package: **SS0001** layout setting after rows were written, **SS0002** `Row()` and `Cells[]` mixed on one sheet, **SS0003** rows written out of order. Warnings at design time instead of exceptions at run time.
- **Pivot tables**: `Worksheet.AddPivotTable` over a table or range written earlier, with row, column, filter and data fields (Sum, Count, Average, Max, Min and more), styles and grand totals. The cache is marked refresh-on-open, so Excel computes the pivot when the file opens; verified against Excel 16 through COM automation (`tests/excel-check-pivot.ps1`).
- **Cached formula results**: `Worksheet.CacheFormulaResults()` opts a sheet into a small evaluator (`+ - * / %`, `SUM`, `AVERAGE`, `COUNT`, `COUNTA`, `MIN`, `MAX`, `ROUND`, `ABS` over cells already written) so viewers that do not calculate show totals instead of blanks. Anything it cannot prove is written without a value, as before.
- **Async reading** (.NET 8+): `ExcelReader.OpenAsync`, `ReadRowsAsync`, `ReadAsync<T>` and `ReadDataTableAsync`, with cancellation between rows; non-seekable uploads are buffered asynchronously and spill to a temp file above 64 MB.
- **Reader**: `ReadMergedRanges` and `ReadHyperlinks` (issue #6) read a sheet's merged cells and links, external URLs resolved from the sheet relationships, without parsing the rows.
- **Benchmarks**: 1,000,000-row and 1,000 × 10,000-column write scenarios, and read benchmarks against MiniExcel, ExcelDataReader, ClosedXML, EPPlus and NPOI.
- **Web sample** (`samples/SheetStream.Sample.Web`): a minimal API with a download button, a 50,000-row streamed export and a `Cells[...]` report with charts; double-click `run.cmd`. A [DotNetFiddle](https://dotnetfiddle.net/BR59ju) runs the quick start in the browser.
Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ 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

```csharp
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](docs/pivot-tables.md).

### Links, comments, rich text and formulas

```csharp
Expand All @@ -142,6 +155,16 @@ sheet.AddComment(2, 3, "Checked by finance", author: "Glenn");
workbook.DefinedNames.Add("TaxRate", "'Settings'!B2");
```

### Formula results for viewers that do not calculate

```csharp
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

```csharp
Expand Down Expand Up @@ -254,6 +277,15 @@ foreach (var link in reader.ReadHyperlinks("Report")) // Url for web/mai

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.

```csharp
// 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](docs/analyzers.md).
Expand All @@ -268,10 +300,10 @@ The package ships a Roslyn analyzer, so the IDE warns before you run: **SS0001**
|---|---|
| `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`, `AddConditionalFormatting`, `AddDataValidation`, `AddHyperlink`, `AddComment`, `Protect`, `PageSetup`, `TabColor`, `Visibility`, `LoadFromCollection` / `DataTable` / `DataReader` |
| `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`, `SheetNames`, `ReadRows`, `Read<T>`, `ReadDataTable`, `ReadMergedRanges`, `ReadHyperlinks` |
| `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` |
Expand All @@ -289,7 +321,7 @@ The package ships a Roslyn analyzer, so the IDE warns before you run: **SS0001**
| Edit and re-save an existing workbook | ✅ | ✅ | ❌ | ✅ (small workbooks, in memory) |
| Read workbooks | ✅ | ✅ | ✅ streaming | ✅ streaming |
| Formula calculation engine | ✅ | ✅ | ❌ | ❌ (Excel calculates on open) |
| Pivot tables | ✅ | ✅ | ❌ | ❌ |
| Pivot tables | ✅ | ✅ | ❌ | ✅ (refreshed on open) |

### Benchmarks

Expand Down Expand Up @@ -318,7 +350,7 @@ Full results, the wide-sheet scenario, methodology, machine details and how to r

- **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 pivot tables, sparklines, macros or encryption.** These are not supported.
- **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.
Expand Down
2 changes: 1 addition & 1 deletion docs/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{ "files": [ "api/**.yml", "api/index.md" ] },
{ "files": [ "*.md", "toc.yml" ], "exclude": [ "_site/**" ] }
],
"resource": [ { "files": [ "images/**" ] } ],
"resource": [ { "files": [ "images/**", "public/**" ] } ],
"dest": "_site",
"template": [ "default", "modern" ],
"globalMetadata": {
Expand Down
Binary file modified docs/images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ chart.AddSeries(values: "C2:C13", categories: "A2:A13", name: "=C1");
package.Save();
```

## Formulas and cached results

`row.WriteFormula("SUM(C2:C101)")` writes the formula and asks Excel to calculate on open. Viewers that do not calculate (file previews, Quick Look, many mobile and web viewers, `ExcelReader`) show such a cell blank unless the file also carries the result. Turn on `sheet.CacheFormulaResults()` before the first row and SheetStream computes the result itself when the formula is simple enough: numbers, references to cells already written on the same sheet, ranges, `+ - * /`, `%`, parentheses and `SUM`, `AVERAGE`, `COUNT`, `COUNTA`, `MIN`, `MAX`, `ROUND`, `ABS`. Anything else (other functions, other sheets, rows not yet written, a division by zero) is written exactly as before, without a value; a wrong value is never written. The sheet keeps about 8 bytes per numeric cell to do this, which is why it is opt-in. When you already know the result, `WriteFormula(formula, cachedValue)` stores it directly.

## Where to go next

- [Migrating from EPPlus](migrating-from-epplus.md): a real exporter converted line by line.
Expand Down
16 changes: 15 additions & 1 deletion docs/migrating-from-epplus.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,18 @@ The file is built into a temporary file and streamed to the response, so memory

## What does not translate

These EPPlus features have no SheetStream equivalent today: formula calculation (`Calculate()`), pivot tables, sparklines, editing an existing workbook in place (other than filling a template), VBA, encryption and digital signatures. If an exporter depends on one of them, keep that exporter on EPPlus 4.5.3.3 (LGPL) or another library.
These EPPlus features have no SheetStream equivalent today: formula calculation (`Calculate()`; SheetStream only writes cached results for simple aggregates, see `CacheFormulaResults`), pivot tables with calculated fields, grouping or slicers (plain pivots are supported and refreshed on open), sparklines, editing large arbitrary workbooks in place (small workbooks can be edited with `ExcelPackage.Edit`; large ones are filled as templates), VBA, encryption and digital signatures. If an exporter depends on one of them, keep that exporter on EPPlus 4.5.3.3 (LGPL) or another library.

## After removing the EPPlus package

Removing EPPlus can break code that never called it. EPPlus 4.5.3.3 depends on `System.Text.Encoding.CodePages` and registers the code-page encoding provider when it loads. Other libraries in the same application may rely on that registration without declaring it; **ExcelDataReader** is the usual case, because legacy `.xls` files need code-page encodings. With EPPlus gone, those imports fail, sometimes silently with empty text.

Register the provider yourself at startup, before the first import:

```csharp
// Program.cs, before building the app. On .NET Framework or netstandard targets add the
// System.Text.Encoding.CodePages package; on .NET 8 it is part of the shared framework.
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
```

Then run one `.xls` import in a test to prove it. This was found while migrating a warehouse system whose exports moved to SheetStream but whose imports stayed on ExcelDataReader. For `.xlsx` imports, `ExcelReader` needs no provider.
Loading