Using EPPlus or ClosedXML for Excel in C Sharp Projects
Choose between EPPlus and ClosedXML for reading and writing xlsx in C Sharp: licensing, API, performance, formulas and when to use the Open XML SDK instead.

.NET developers who need to produce or read an Excel file without installing Excel on a server have a short list of libraries. EPPlus and ClosedXML are the two names that come up most, and the choice between them turns less on code than on licensing, size of data and habit. This post compares them on what matters, shows the common read and write operations in each, and says when to drop down to the Open XML SDK. It is a spoke of reading and writing Excel from JavaScript, Java and C Sharp.
What they are
ClosedXML is an open-source library under the MIT license. It sits on top of Microsoft's Open XML SDK and offers a friendly object model of workbooks, worksheets, ranges and cells.
EPPlus is a long-established library with its own implementation. Versions up to 4 were LGPL. From version 5, EPPlus moved to the Polyform Noncommercial license: free for noncommercial use, with a paid commercial license for companies. It has a large feature set, including charts, pivot tables, formula calculation and performance work.
The Open XML SDK is Microsoft's low-level library for the file format: it exposes the XML parts directly. It is verbose, very fast and streams, and ClosedXML builds on it.
Neither EPPlus nor ClosedXML reads the old .xls format. For that, use NPOI, a .NET port of Apache POI, or convert the files first (batch converting XLS files to XLSX).
Licensing first
Choose after reading the current terms, because they decide whether EPPlus is available to you. The practical summary as of the versions named above: ClosedXML is free for commercial use under MIT; EPPlus 5 and later require a commercial license when used in a commercial setting, with a license context set in code or configuration (in EPPlus 7, the non-commercial case is declared with ExcelPackage.License.SetNonCommercialPersonal(...) or an organization variant, and the commercial case with a license key). Using EPPlus in a commercial product without a license is a legal exposure, and a surprise at audit time. If the license is a problem, ClosedXML or the Open XML SDK are free alternatives. Check the vendor's pages for the current wording and prices.
Read with ClosedXML
using ClosedXML.Excel;
using var wb = new XLWorkbook("orders.xlsx");
var ws = wb.Worksheet("Orders"); // or wb.Worksheet(1)
foreach (var row in ws.RangeUsed().RowsUsed().Skip(1)) // skip the header
{
string order = row.Cell(1).GetString();
DateTime date = row.Cell(2).GetDateTime();
string cust = row.Cell(3).GetString();
double amount = row.Cell(4).GetDouble();
}Rows and columns are one-based, as in Excel. RangeUsed() returns the block that contains data, and is null for an empty sheet, so check. GetString, GetDouble and GetDateTime throw when the cell holds something that does not convert, which is useful for strict imports and harmful for messy files. Use TryGetValue<T>(out var v) or check cell.DataType and cell.IsEmpty() first:
var c = row.Cell(4);
if (c.IsEmpty()) { /* missing amount */ }
else if (c.DataType == XLDataType.Number) { var amount = c.GetDouble(); }
else { /* text in a number column: report row.RowNumber() */ }For a formula cell, GetValue<T>() returns the cached result when one exists; ClosedXML can also calculate many formulas on demand (cell.Value triggers evaluation in some versions), with limits on supported functions.
Convert a range to a typed list by mapping each row, or read into a DataTable with ws.RangeUsed().AsTable().AsNativeDataTable() for a quick import.
Read with EPPlus
using OfficeOpenXml;
ExcelPackage.License.SetNonCommercialPersonal("Your Name"); // EPPlus 8 style; EPPlus 5-7 use LicenseContext
using var pkg = new ExcelPackage(new FileInfo("orders.xlsx"));
var ws = pkg.Workbook.Worksheets["Orders"];
for (int r = 2; r <= ws.Dimension.End.Row; r++)
{
string order = ws.Cells[r, 1].Text;
var date = ws.Cells[r, 2].GetValue<DateTime>();
double amt = ws.Cells[r, 4].GetValue<double>();
}The license line differs by version; use the one in the documentation for the version you installed, and configure it once at startup, not per call. ws.Dimension is null for an empty sheet. Cells[r, c].Text returns the displayed text, Value the object, and GetValue<T>() converts, returning the default for blanks. EPPlus also has LoadFromCollection and LoadFromDataTable for writing, and ToDataTable for reading in recent versions.
Write a report with ClosedXML
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Report");
ws.Cell(1, 1).Value = "Order";
ws.Cell(1, 2).Value = "Date";
ws.Cell(1, 3).Value = "Amount";
ws.Range(1, 1, 1, 3).Style.Font.Bold = true;
ws.Cell(2, 1).Value = "0007"; // keeps the leading zeros
ws.Cell(2, 2).Value = new DateTime(2025, 3, 14);
ws.Cell(2, 2).Style.DateFormat.Format = "yyyy-mm-dd";
ws.Cell(2, 3).Value = 1234.5;
ws.Cell(2, 3).Style.NumberFormat.Format = "#,##0.00";
ws.SheetView.FreezeRows(1);
ws.RangeUsed().SetAutoFilter();
ws.Columns().AdjustToContents();
wb.SaveAs("report.xlsx");Assigning a string value keeps it as text; assigning a number or a DateTime writes the proper type. InsertTable places a collection or a DataTable as an Excel Table with styling in one call. AdjustToContents measures cell text and can be slow on big sheets.
Write with EPPlus
using var pkg = new ExcelPackage();
var ws = pkg.Workbook.Worksheets.Add("Report");
ws.Cells["A1"].LoadFromCollection(orders, PrintHeaders: true, TableStyle: OfficeOpenXml.Table.TableStyles.Medium2);
ws.Cells["B:B"].Style.Numberformat.Format = "yyyy-mm-dd";
ws.Cells["C:C"].Style.Numberformat.Format = "#,##0.00";
ws.Cells[ws.Dimension.Address].AutoFitColumns();
pkg.SaveAs(new FileInfo("report.xlsx"));LoadFromCollection maps public properties to columns and adds a table. EPPlus is known for good speed on bulk loads and a broad feature set (charts, conditional formatting, pivot tables, data validation).
Comparing them
| ClosedXML | EPPlus | |
|---|---|---|
| License | MIT | Polyform Noncommercial; commercial license for business use (5+) |
| API feel | Fluent, friendly | Rich, Excel-like cell addressing |
| Basis | Open XML SDK | Own implementation |
| Speed | Good | Often faster for large loads |
| Features | Wide; fewer advanced objects | Very wide (charts, pivots, calculation) |
| Streaming | No | Limited; load/save in memory |
| Formula calculation | Basic engine | Calculation engine |
Treat the speed row as a hypothesis to test on your data. Both load the workbook into memory, so they share the memory problem: hundreds of thousands of rows with formatting can need gigabytes.
Large files: the Open XML SDK
For huge reads or writes, use the SDK's SAX-style reader and writer, which stream through the XML without building the object model:
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using var doc = SpreadsheetDocument.Open("big.xlsx", false);
var wbPart = doc.WorkbookPart!;
var sst = wbPart.SharedStringTablePart?.SharedStringTable;
var wsPart = wbPart.WorksheetParts.First();
using var reader = OpenXmlReader.Create(wsPart);
while (reader.Read())
{
if (reader.ElementType == typeof(Row) && reader.IsStartElement)
{
var row = (Row)reader.LoadCurrentElement()!;
foreach (Cell c in row.Elements<Cell>())
{
// c.CellValue?.Text; if c.DataType == CellValues.SharedString, index into sst
}
}
}It is more code, and constant memory. Shared string indexes and number formats must be handled by you. Use it for the occasional giant file, and ClosedXML or EPPlus for everything else (reading a 500 MB Excel file without running out of memory explains the problem).
Safety and checks
Files from outside are untrusted. Limit the upload size, set timeouts, do not load macros (xlsm macros are not executed by these libraries, but should be rejected if not expected), and validate the structure. Neutralize cell text that starts with =, +, - or @ before writing user data, or Excel may run it as a formula.
After reading, compare the row count and a total with the workbook. Test a blank cell, a date, a text number, a formula and a merged range. Open generated files in Excel, not just in the library.
Dates, types and common exceptions
A few behaviors catch people in both libraries. Excel dates are numbers with a format, and both libraries convert a date-formatted cell to a DateTime when you ask for one. If the cell is not date-formatted, asking for a date throws or returns a wrong value, so check the type first. A cell that holds text that looks like a date stays text, and parsing it needs an explicit format and culture. DateTime has no time zone; if the source implies one, record it in the schema, not in the value.
Numbers are doubles. Money should be converted to decimal at the boundary, with rounding stated, and never accumulated as double across thousands of rows. Integers above 2^53 lose precision, and Excel keeps 15 significant digits, so identifiers must be text.
Typical exceptions: an InvalidCastException or FormatException when a cell holds text where a number was expected; a NullReferenceException when a worksheet name is wrong or the used range is null on an empty sheet; an InvalidDataException or an Open XML error when the file is not a valid xlsx, for instance a CSV renamed to .xlsx or an encrypted workbook. Check the first bytes of the upload before opening (why Excel says the file format or extension is not valid) and catch the exceptions at the import boundary with a message that says which row and column failed.
For a strict import, read each row into a small record type with validation, collect all errors and report them together, so the person fixing the file sees the whole list in one go and not one problem per upload.
When neither EPPlus nor ClosedXML is the right choice
You need the old xls. Use NPOI.
The data is huge. Use the Open XML SDK streaming, or convert to CSV or Parquet outside .NET.
You only write a simple export. CSV needs no library.
You need to calculate everything Excel can. Neither matches Excel's full engine; for exact results, calculate in code or use a spreadsheet application.
See the ClosedXML documentation, the EPPlus site for licensing and API, and Microsoft's Open XML SDK guide. To convert a small result to JSON, use the CSV to JSON converter.


