CLI tool
sveinungf/spreadcheetah avatar
sveinungf/spreadcheetah

SpreadCheetah: streaming XLSX generation in .NET

SpreadCheetah is a high-performance .NET library for generating spreadsheet (Microsoft Excel XLSX) files.

710 stars33 forksC#MIT

At a glance

What is it?
SpreadCheetah is a C# library for writing Excel XLSX files in a forward-only, streaming manner, with a source generator that turns plain objects into worksheet rows. It has no dependency on Microsoft Excel and targets .NET Standard 2.0 through .NET 10.
Who is it for?
SpreadCheetah fits .NET teams that have to emit large Excel files, such as exports from a reporting service, and want a low allocation path without installing Excel or a heavyweight object to model library. It is the wrong choice if you need to read and edit existing workbooks, or if you need spreadsheet features the supported list does not yet cover.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A writer that only moves forward

SpreadCheetah is a .NET library for generating Excel XLSX files, and its central design decision is stated plainly in the README: the library is designed to create spreadsheet files in a forward-only manner. Worksheets are written from left to right, rows from top to bottom, and the cells of a row from left to right. That constraint is what allows the file to be produced in a streaming manner, and the README connects it directly to a low memory footprint, which is the property that matters when the export runs on a service that also has to answer requests. The library is written in C#, has no dependency on Microsoft Excel, and supports .NET Standard 2.0 targets such as .NET Framework, along with .NET 6 through .NET 10, where later versions get more optimizations. It is trimmable and NativeAOT compatible, and it exposes async APIs. Basic spreadsheet functionality is covered, including cells with different data types, styling and formulas, with a link to a wiki page listing exactly what is currently supported.

Creating a spreadsheet, and the Finish call you must not forget

The usage pattern is a stream, a worksheet and a sequence of rows. The README's basic example creates a spreadsheet over a stream, starts a worksheet, adds a row of cells and finishes the file:

cs
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>
    {
        new Cell("Answer to the ultimate question:"),
        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();
}

Two details in that snippet are worth more than the surrounding prose. A spreadsheet must contain at least one worksheet, so `StartWorksheetAsync` is not optional. And the comment on `FinishAsync` is a warning about a real failure mode: the XLSX file has to be finalized before the object is disposed, and the README flags this as important to properly finalize the file. A writer that streams out rows but is never finished produces a document Excel will not open properly, and the failure appears at the end of a long export rather than at the start.

The source generator turns properties into cells

SpreadCheetah includes a C# source generator that makes it easier to create rows from objects, and the README positions it as similar in use to the System.Text.Json source generator. The model class is an ordinary class, and the generator picks up the properties with public getters, using their declaration order to decide the order of the cells:

cs
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; }
}

You then declare a partial class with a `WorksheetRow` attribute carrying the target type, and the compiler emits the implementation during the build:

cs
using SpreadCheetah.SourceGeneration;

namespace MyNamespace;

[WorksheetRow(typeof(MyObject))]
public partial class MyObjectRowContext : WorksheetRowContext;

Rows are then written with `AddAsRowAsync`, passing the object and the context type. The generator works with classes, records and structs, and can be used on all supported .NET versions including .NET Framework, but the C# version must be 12 or greater. That last requirement is a real constraint for teams on older language versions, since the rest of the library has no such requirement.

What the generated code actually does at runtime

The README shows part of the generated implementation, and it explains why the library advertises low memory allocation. The generated method rents a buffer from a shared pool and returns it in a finally block:

cs
// <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);
    }
}

Renting a buffer rather than allocating a new array per row is what keeps the generated path allocation free on the hot path, and the `ConfigureAwait(false)` is there because the library is doing asynchronous IO. A caller sees `AddAsRowAsync` and gets that behaviour for free, which is the point of a source generator over a reflection based mapper: the cost is paid at compile time instead of at runtime, and the mapping is checked by the compiler.

Getting the package and the samples to read

SpreadCheetah is on nuget.org, and the README gives two ways to add it. The .NET CLI route is one command:

bash
dotnet add package SpreadCheetah

The Package Manager route used from Visual Studio or the Package Manager Console is the other:

bash
Install-Package SpreadCheetah

For the wider picture the README points at a separate samples repository with runnable examples for writing to a file, styling basics, formula basics, DateTime and formatting, data validations and performance tips, and at wiki pages for tables and for adding an embedded image. A Getting started guide sits in the same wiki. The project follows Semantic Versioning and the release notes live on the Releases page, and the badges in the README show NuGet version information alongside the GitHub Actions workflow status and a Codecov report, so the test and coverage story is visible from the front page.

The benchmark table and what it does and does not tell you

The README backs the performance claim with a BenchmarkDotNet run and lists the libraries it compares against. The published configuration is specific about the machine, the runtime and the host, and it is worth reading before you read the numbers:

``` ini BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2) AMD Ryzen 7 9800X3D 4.70GHz, 1 CPU, 16 logical and 8 physical cores [Host] : .NET Framework 4.8.1 (4.8.9221.0), X64 RyuJIT VectorSize=256 Job-CZIXIP : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v4 Job-RRVADO : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4 Job-LYYWPY : .NET Framework 4.8.1 (4.8.9221.0), X64 RyuJIT VectorSize=256

BuildConfiguration=Benchmark InvocationCount=1 UnrollFactor=1 ```

Two things stand out. The comparison runs across three runtimes, .NET 10, .NET 8 and .NET Framework 4.8.1, so the library's cross version support is exercised rather than assumed. And the invocation count is 1 with an unroll factor of 1, so these are single-shot measurements of large operations rather than microbenchmarks of a tight loop. The benchmark projects are in the repository, in `SpreadCheetah.Benchmark/` and `SpreadCheetah.Benchmark.Test/`, so the numbers can be reproduced on your own hardware, which is the only way they become useful to you.

Where the forward-only design costs you

The streaming model has a direct consequence for what you can build. You can add rows in order, but you cannot go back and change a cell you already wrote, because the file is being produced as a stream. Any export that needs random access, or needs a value that depends on the whole dataset before the first row is written, does not fit the model. The other boundary is feature coverage. The README says most basic spreadsheet functionality is supported, such as cells with different data types, styling and formulas, and that more advanced functionality will be added in future releases, with the current list in the wiki. So the question for an adopter is not whether the library can write a styled cell, but whether it supports the specific feature your report needs, and the answer lives in that wiki list rather than in the README. There is also no reading path here. SpreadCheetah writes XLSX files; if your requirement is to open an existing workbook, change a value and save it back, this is the wrong library.

Editorial conclusion

SpreadCheetah fits .NET teams that have to emit large Excel files, such as exports from a reporting service, and want a low allocation path without installing Excel or a heavyweight object to model library. It is the wrong choice if you need to read and edit existing workbooks, or if you need spreadsheet features the supported list does not yet cover. Before you adopt it, read the wiki page listing currently supported spreadsheet functionality, check that your target framework is in the supported range from .NET Standard 2.0 to .NET 10, and confirm that the C# version is 12 or higher if you plan to use the source generator. The last push was on 2026-10-14 and version 1.28.0 was released on 2026-07-02, under the MIT licence.

Frequently asked questions

How do I install SpreadCheetah in a .NET project?

The package is on nuget.org and can be added with `dotnet add package SpreadCheetah` from the .NET CLI, or with `Install-Package SpreadCheetah` from the Package Manager.

Which .NET versions does SpreadCheetah support?

The library supports .NET Standard 2.0 targets such as .NET Framework, and .NET 6 through .NET 10, with more optimizations enabled for the later versions. It is also trimmable and NativeAOT compatible.

Does SpreadCheetah need Microsoft Excel installed?

No. The README lists having no dependency to Microsoft Excel as one of the features, and the library writes XLSX files directly in a forward-only streaming manner.

What does the SpreadCheetah source generator do?

It generates code at build time that creates a worksheet row from an object, picking the properties with public getters and using their order for the cell order. You declare a partial class with the WorksheetRow attribute and then call AddAsRowAsync with the object and the context type.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sveinungf/spreadcheetah on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sveinungf-spreadcheetah.svg)](https://hysenlabs.com/projects/sveinungf-spreadcheetah)

Was this useful?