spreadcheetah
spreadcheetah copied to clipboard
SpreadCheetah is a high-performance .NET library for generating spreadsheet (Microsoft Excel XLSX) files.
SpreadCheetah
SpreadCheetah is a high-performance .NET library for generating spreadsheet (Microsoft Excel XLSX) files.
Features
- Performance (see benchmarks below)
- Low memory allocation (see benchmarks below)
- Async APIs
- No dependency to Microsoft Excel
- Targeting .NET Standard 2.0 (for .NET Framework 4.6.1 and later)
- Free and open source!
SpreadCheetah is designed to create spreadsheet files in a forward-only manner. That means worksheets from left to right, rows from top to bottom, and row cells from left to right. This allows for creating spreadsheet files in a streaming manner, while also keeping a low memory footprint.
Most basic spreadsheet functionality is supported, such as cells with different data types, basic styling, and formulas. More advanced functionality is planned for future releases.
How to install
SpreadCheetah is available as a NuGet package. The NuGet package targets .NET Standard 2.0 as well as newer versions of .NET. The .NET Standard 2.0 version is just intended for backwards compatibility (.NET Framework and earlier versions of .NET Core). More optimizations are enabled when targeting newer versions of .NET.
Basic usage
using (var spreadsheet = await Spreadsheet.CreateNewAsync(stream))
{
// A spreadsheet must contain at least one worksheet.
await spreadsheet.StartWorksheetAsync("Sheet 1");
// Cells are inserted row by row.
var row = new List<Cell>();
row.Add(new Cell("Answer to the ultimate question:"));
row.Add(new Cell(42));
// Rows are inserted from top to bottom.
await spreadsheet.AddRowAsync(row);
// Remember to call Finish before disposing.
// This is important to properly finalize the XLSX file.
await spreadsheet.FinishAsync();
}
Other examples
Using the Source Generator
Source Generators is a newly released feature in the C# compiler. SpreadCheetah includes a source generator that makes it easier to create rows from objects. It is used in a similar way to the System.Text.Json
source generator:
namespace MyNamespace;
// A plain old C# class which we want to add as a row in a worksheet.
// The source generator will pick the properties with public getters.
// The order of the properties will decide the order of the cells.
public class MyObject
{
public string Question { get; set; }
public int Answer { get; set; }
}
The source generator will be instructed to generate code by defining a partial class like this:
using SpreadCheetah.SourceGeneration;
namespace MyNamespace;
[WorksheetRow(typeof(MyObject))]
public partial class MyObjectRowContext : WorksheetRowContext
{
}
During build, the type will be analyzed and an implementation of the context class will be created. We can then create a row from an object by calling AddAsRowAsync
with the object and the context type as parameters:
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream);
await spreadsheet.StartWorksheetAsync("Sheet 1");
var myObj = new MyObject { Question = "How many Rings of Power were there?", Answer = 20 };
await spreadsheet.AddAsRowAsync(myObj, MyObjectRowContext.Default.MyObject);
await spreadsheet.FinishAsync();
Here is a peek at part of the code that was generated for this example:
// <auto-generated />
private static async ValueTask AddAsRowInternalAsync(Spreadsheet spreadsheet, MyObject obj, CancellationToken token)
{
var cells = ArrayPool<DataCell>.Shared.Rent(2);
try
{
cells[0] = new DataCell(obj.Question);
cells[1] = new DataCell(obj.Answer);
await spreadsheet.AddRowAsync(cells.AsMemory(0, 2), token).ConfigureAwait(false);
}
finally
{
ArrayPool<DataCell>.Shared.Return(cells, true);
}
}
The source generator can generate rows from classes, records, and structs. It can be used in all supported .NET versions, including .NET Framework, however the C# version must be 8.0 or greater.
Benchmarks
The benchmark results here have been collected using Benchmark.NET with the following system configuration:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1645 (21H1/May2021Update)
Intel Core i5-8600K CPU 3.60GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores
.NET SDK=6.0.300-preview.22154.4
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
.NET 6.0 : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.24 (CoreCLR 4.700.22.16002, CoreFX 4.700.22.17909), X64 RyuJIT
.NET Framework 4.8 : .NET Framework 4.8 (4.8.4470.0), X64 RyuJIT
InvocationCount=1 UnrollFactor=1
The code executed in the benchmark creates a worksheet of 20 000 rows and 10 columns filled with string values. The same use case has been implemented in other spreadsheet libraries for comparison. Some of these libraries have multiple ways of achieving the same result, but to make this a fair comparison the idea is to use the most efficient approach for each library. The code is available here.
.NET Framework 4.8
Library | Mean | Error | StdDev | Allocated |
---|---|---|---|---|
SpreadCheetah | 63.87 ms | 0.220 ms | 0.195 ms | 160 KB |
Open XML (SAX approach) | 414.49 ms | 5.353 ms | 4.745 ms | 43 317 KB |
EPPlus v4 | 594.07 ms | 3.749 ms | 3.507 ms | 286 368 KB |
Open XML (DOM approach) | 984.10 ms | 11.376 ms | 10.085 ms | 160 371 KB |
ClosedXML | 2,397.98 ms | 16.359 ms | 14.502 ms | 589 548 KB |
.NET Core 3.1
Library | Mean | Error | StdDev | Allocated |
---|---|---|---|---|
SpreadCheetah | 32.28 ms | 0.271 ms | 0.253 ms | 51 KB |
Open XML (SAX approach) | 219.18 ms | 0.708 ms | 0.591 ms | 66 049 KB |
EPPlus v4 | 470.28 ms | 1.776 ms | 1.483 ms | 216 127 KB |
Open XML (DOM approach) | 786.81 ms | 12.227 ms | 10.839 ms | 182 304 KB |
ClosedXML | 2,130.32 ms | 9.350 ms | 8.746 ms | 561 807 KB |
.NET 6
Library | Mean | Error | StdDev | Allocated |
---|---|---|---|---|
SpreadCheetah | 28.75 ms | 0.108 ms | 0.101 ms | 6 KB |
Open XML (SAX approach) | 220.46 ms | 0.825 ms | 0.772 ms | 66 060 KB |
EPPlus v4 | 403.14 ms | 4.207 ms | 3.936 ms | 195 792 KB |
Open XML (DOM approach) | 720.64 ms | 14.178 ms | 13.925 ms | 182 301 KB |
ClosedXML | 1,876.50 ms | 16.333 ms | 15.278 ms | 550 775 KB |