If you have ever tried to generate an Excel report or import a spreadsheet from a .NET service, you have probably hit the same wall as everyone else: Microsoft.Office.Interop.Excel requires Office to be installed, wonโt run on a server, and is unsupported in headless or container environments.
This article walks through the practical alternatives for working with .xlsx files in C# โ what each library is good at, where the rough edges are, and which one to pick for which job.
Why not Office Interop?
Microsoft.Office.Interop.Excel automates a real Excel process via COM. It works on a developer machine but is a bad fit for almost any production scenario:
- Excel must be installed on the host
- Microsoft explicitly does not support it in services or web apps (KB257757)
- Each call crosses a COM boundary, so it is slow
- Crashed Excel processes leak and accumulate over time
For server-side or cross-platform code, you want a library that reads and writes the OpenXML .xlsx format directly.
The contenders
| Library | License | What itโs for |
|---|---|---|
| OpenXML SDK | MIT | Low-level read/write of the raw OpenXML parts |
| ClosedXML | MIT | High-level wrapper over OpenXML SDK โ simple API |
| EPPlus | Polyform Noncommercial / Commercial | Full-featured xlsx, charts, pivots; paid for commercial use since v5 |
| NPOI | Apache 2.0 | Java POI port โ handles both legacy .xls and .xlsx |
| ReoGrid | Free / Commercial | Read/write and display/edit the same file in WinForms/WPF |
The โrightโ choice depends on whether you also need to show the spreadsheet to a user, not just parse it.
Reading a workbook
Suppose you have an invoices.xlsx and you want to print every row in the first sheet.
OpenXML SDK (low-level)
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using var doc = SpreadsheetDocument.Open("invoices.xlsx", false);
var sheet = doc.WorkbookPart!.Workbook.Sheets!.Elements<Sheet>().First();
var part = (WorksheetPart)doc.WorkbookPart.GetPartById(sheet.Id!);
var rows = part.Worksheet.Descendants<Row>();
foreach (var row in rows)
{
foreach (var cell in row.Elements<Cell>())
{
Console.Write(cell.CellValue?.Text + "\t");
}
Console.WriteLine();
}
This is the official Microsoft library and itโs free, but you spend most of your time fighting the format: shared strings, styles indexed by position, inline strings, and so on. Use it when you need bit-level control.
ClosedXML (high-level wrapper)
using ClosedXML.Excel;
using var book = new XLWorkbook("invoices.xlsx");
var sheet = book.Worksheet(1);
foreach (var row in sheet.RowsUsed())
{
foreach (var cell in row.CellsUsed())
{
Console.Write(cell.GetString() + "\t");
}
Console.WriteLine();
}
This is the experience most people want. The trade-off is that ClosedXML loads the whole workbook into memory and is slower than streaming readers for very large files.
ReoGrid (when you also display the data)
using unvell.ReoGrid;
var book = ReoGridControl.CreateMemoryWorkbook();
book.Load("invoices.xlsx");
var sheet = book.Worksheets[0];
for (int r = 0; r <= sheet.MaxContentRow; r++)
{
for (int c = 0; c <= sheet.MaxContentCol; c++)
{
Console.Write(sheet[r, c] + "\t");
}
Console.WriteLine();
}
Same workbook object you bind to the WinForms/WPF grid control, so there is no separate โview modelโ layer.
Writing a workbook
Now the inverse โ generate a small report with a header row, a few data rows, and an Excel formula.
ClosedXML
using var book = new XLWorkbook();
var sheet = book.Worksheets.Add("Report");
sheet.Cell("A1").Value = "Product";
sheet.Cell("B1").Value = "Units";
sheet.Cell("C1").Value = "Price";
sheet.Cell("D1").Value = "Total";
sheet.Range("A1:D1").Style.Font.Bold = true;
sheet.Cell("A2").Value = "Widget";
sheet.Cell("B2").Value = 12;
sheet.Cell("C2").Value = 9.99;
sheet.Cell("D2").FormulaA1 = "=B2*C2";
book.SaveAs("report.xlsx");
ReoGrid
var book = ReoGridControl.CreateMemoryWorkbook();
var sheet = book.CreateWorksheet("Report");
book.Worksheets.Add(sheet);
sheet["A1"] = new object[] { "Product", "Units", "Price", "Total" };
sheet.Ranges["A1:D1"].Style.Bold = true;
sheet["A2"] = new object[] { "Widget", 12, 9.99 };
sheet["D2"] = "=B2*C2";
book.Save("report.xlsx");
Both libraries handle column widths, fills, borders, number formats, and merged cells the way you would expect. The biggest difference is that ReoGrid evaluates the formula at write time using its built-in formula engine, so D2 already contains 119.88 when the file lands on disk. With ClosedXML you need book.RecalculateAllFormulas() first if you depend on cached values.
What about really large files?
For files where you cannot afford to load everything into memory:
- OpenXML SDK has the
OpenXmlReaderfor streaming โ verbose but cheap. - EPPlus has a streaming write API that flushes rows as you append them.
- ReoGrid 4.4 added bulk loading via
SetRangeDatathat is around 3ร faster than per-cell assignment for 200,000-cell loads โ see the v4.4 release notes for measured numbers.
Rule of thumb: under 50,000 rows the friendly APIs are fine; above that, stream.
Picking a library
A short decision guide:
- Just need to parse a known-format xlsx? โ ClosedXML or NPOI. Both are MIT/Apache and easy.
- Need full control over OpenXML internals? โ OpenXML SDK.
- Need to also display and edit the file in a desktop app? โ ReoGrid (or any spreadsheet UI control).
- Need charts, pivots, conditional formatting, all on the server? โ EPPlus (commercial license required) or ReoGrid.
- Mixing
.xlsand.xlsx? โ NPOI is the only one with first-class legacy.xlssupport.
There isnโt one library that wins on every axis. For headless reporting in a service, ClosedXML is hard to beat. For a desktop application that loads, edits, and saves the same workbook, having one library that does both โ read/write and UI โ is much less code than gluing two libraries together.