Работа с Excel в C#: библиотеки и примеры
Excel-файлы в C# читают и создают двумя путями: через библиотеки для формата xlsx, которым не нужен установленный Office, или через COM-интеграцию Interop, которая управляет настоящим Excel. Для сервера и обычных отчётов подходят ClosedXML, EPPlus и NPOI, а Interop нужен в редких задачах с макросами и диаграммами. Ниже сравнение вариантов и рабочий код для .NET 8.

Содержание
Как работать с Excel из C#: обзор подходов
Файл xlsx - это архив с XML-документами, поэтому его можно собрать без самого Excel. Именно так работают современные библиотеки. Старый формат xls (до версии 2007) использует бинарную структуру, его поддерживает NPOI. Вторая группа - Interop: код запускает процесс Excel и дирижирует им через COM. Это медленно, требует лицензионного Office на машине и не годится для веб-сервера. Выбор зависит от задачи.
| Способ | Форматы | Нужен Excel | Лицензия | Когда выбрать |
|---|---|---|---|---|
| ClosedXML | xlsx | нет | MIT, бесплатно | отчёты, импорт и экспорт, простой API |
| EPPlus | xlsx | нет | Polyform Noncommercial, для коммерции платная | богатые стили, диаграммы, большие файлы |
| NPOI | xls, xlsx | нет | Apache 2.0 | работа со старыми xls |
| Open XML SDK | xlsx | нет | MIT | низкоуровневый контроль и потоковая запись |
| ExcelDataReader, MiniExcel | xls, xlsx, csv | нет | MIT, Apache | быстрое чтение больших таблиц |
| Interop | всё, что открывает Excel | да | лицензия Office | макросы, графики, автоматизация на рабочем месте |
Если нужно просто записать данные в таблицу или прочитать их, начинайте с ClosedXML: у него самый понятный интерфейс и нет вопросов к лицензии. EPPlus сильнее в стилях и диаграммах, но с пятой версии требует лицензию для коммерческого использования. Разберём оба на практике, а затем Interop.
ClosedXML: запись и чтение
Подключите пакет командой dotnet add package ClosedXML. Основные объекты: XLWorkbook (книга), IXLWorksheet (лист) и ячейка, к которой обращаются по адресу вроде "A1" или по номерам строки и столбца, начиная с единицы.
using ClosedXML.Excel;
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Заказы");
sheet.Cell("A1").Value = "Товар";
sheet.Cell("B1").Value = "Количество";
sheet.Cell("C1").Value = "Цена";
sheet.Cell("A2").Value = "Клавиатура";
sheet.Cell("B2").Value = 3;
sheet.Cell("C2").Value = 1990.50;
sheet.Cell(3, 1).Value = "Мышь";
sheet.Cell(3, 2).Value = 5;
sheet.Cell(3, 3).Value = 790;
workbook.SaveAs("orders.xlsx");Чтение выглядит зеркально. Метод RangeUsed() возвращает область с данными, а перебор строк помогает не думать о пустых ячейках внизу. Для получения значения нужного типа лучше использовать GetString(), GetDouble(), GetDateTime() или универсальный GetValue<T>(). Они выдадут понятную ошибку, если в ячейке лежит что-то другое.
using var workbook = new XLWorkbook("orders.xlsx");
var sheet = workbook.Worksheet(1); // нумерация листов с единицы
foreach (var row in sheet.RangeUsed()!.RowsUsed().Skip(1)) // пропускаем заголовок
{
string name = row.Cell(1).GetString();
int qty = row.Cell(2).GetValue<int>();
decimal price = row.Cell(3).GetValue<decimal>();
Console.WriteLine($"{name}: {qty} x {price} = {qty * price}");
}Типичная проблема при чтении - пустые или текстовые ячейки там, где ожидается число. Проверяйте cell.IsEmpty() и пользуйтесь методом TryGetValue<T>(out T), чтобы не получить исключение из-за одной кривой строки в пользовательском файле.
Выгрузка списка объектов и таблицы базы
Писать ячейки по одной нужно только для небольших форм. Коллекции объектов выгружаются одной командой. Метод InsertTable создаёт таблицу Excel с заголовками, фильтром и стилем, где названия столбцов берутся из имён свойств класса. Для DataTable есть отдельная перегрузка добавления листа.
public record Product(string Name, int Quantity, decimal Price);
var products = new List<Product>
{
new("Клавиатура", 3, 1990.5m),
new("Мышь", 5, 790m),
new("Монитор", 2, 15990m)
};
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Товары");
sheet.Cell(1, 1).InsertTable(products);
sheet.Columns().AdjustToContents();
workbook.SaveAs("products.xlsx");
// DataTable (например, из SqlDataAdapter)
// workbook.Worksheets.Add(dataTable, "Выгрузка");Метод AdjustToContents() подбирает ширину столбцов по содержимому. Для больших объёмов вызывайте его только один раз для всего листа и желательно на ограниченном диапазоне, потому что автоподбор по десяткам тысяч строк заметно замедляет сохранение.
Форматирование, формулы и даты
Отчёт без оформления читать тяжело, а ошибки в форматах чисел ведут к настоящим потерям: цены без копеек, даты в виде серийных чисел, длинные коды в научной нотации. Все свойства оформления доступны через Style ячейки или диапазона.
using ClosedXML.Excel;
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Счёт");
var header = sheet.Range("A1:D1");
header.Style.Font.Bold = true;
header.Style.Fill.BackgroundColor = XLColor.LightGray;
header.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
sheet.Cell("A1").Value = "Дата";
sheet.Cell("B1").Value = "Товар";
sheet.Cell("C1").Value = "Кол-во";
sheet.Cell("D1").Value = "Сумма";
sheet.Cell("A2").Value = DateTime.Today;
sheet.Cell("A2").Style.DateFormat.Format = "dd.MM.yyyy";
sheet.Cell("B2").Value = "Монитор";
sheet.Cell("C2").Value = 2;
sheet.Cell("D2").FormulaA1 = "C2*15990";
sheet.Cell("D2").Style.NumberFormat.Format = "#,##0.00 \"₽\"";
sheet.Cell("D4").FormulaA1 = "SUM(D2:D3)";
sheet.Cell("D4").Style.Font.Bold = true;
// длинный код как текст, чтобы Excel не превратил его в число
sheet.Cell("B5").Value = "0012345678901";
sheet.Cell("B5").Style.NumberFormat.Format = "@";
sheet.Columns().AdjustToContents();
workbook.SaveAs("invoice.xlsx");Формулы записываются на английском с разделителем-запятой, независимо от языка интерфейса пользователя. Excel пересчитает их при открытии файла. Если значение нужно прочитать в программе сразу, свойство Value ячейки с формулой ClosedXML вычисляет самостоятельно, но поддерживаются не все функции, поэтому сложные расчёты безопаснее вести в коде.
EPPlus: создание и чтение xlsx
EPPlus устанавливается пакетом EPPlus. Начиная с пятой версии библиотека распространяется по лицензии Polyform Noncommercial: для личных проектов и обучения она бесплатна, для коммерческих нужна покупка. Перед первым использованием нужно указать контекст лицензии, иначе получите исключение. В версии 8 способ указания изменился на ExcelPackage.License.SetNonCommercialPersonal("Имя"), в версиях 5-7 используется свойство LicenseContext, как показано ниже.
using OfficeOpenXml;
ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // EPPlus 5-7
using var package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("Данные");
sheet.Cells["A1"].Value = "Город";
sheet.Cells["B1"].Value = "Население, тыс.";
sheet.Cells["A2"].Value = "Казань";
sheet.Cells["B2"].Value = 1310;
sheet.Cells["A3"].Value = "Самара";
sheet.Cells["B3"].Value = 1140;
sheet.Cells["A1:B1"].Style.Font.Bold = true;
sheet.Cells["B4"].Formula = "SUM(B2:B3)";
sheet.Cells.AutoFitColumns();
package.SaveAs(new FileInfo("cities.xlsx"));using var package = new ExcelPackage(new FileInfo("cities.xlsx"));
var sheet = package.Workbook.Worksheets[0];
int lastRow = sheet.Dimension.End.Row;
for (int row = 2; row <= lastRow; row++)
{
string city = sheet.Cells[row, 1].Text;
if (int.TryParse(sheet.Cells[row, 2].Text, out int population))
Console.WriteLine($"{city}: {population}");
}Свойство Text возвращает значение так, как оно показано в Excel, то есть с применённым форматом, а Value отдаёт сырой объект. Для чисел и дат надёжнее брать Value и приводить тип самому. Для массовой загрузки у EPPlus есть LoadFromCollection и LoadFromDataTable, они в разы быстрее записи по ячейкам.
sheet.Cells["A1"].LoadFromCollection(products, printHeaders: true);Interop: управление настоящим Excel
Interop подключается как COM-ссылка на Microsoft Excel Object Library и запускает отдельный процесс EXCEL.EXE. Он нужен, когда требуются возможности самого приложения: макросы VBA, сводные таблицы, печать, конвертация в PDF средствами Excel. Платформа ограничена Windows и установленным Office. В службах и веб-приложениях он не поддерживается Microsoft.
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
Excel.Application? app = null;
Excel.Workbook? book = null;
Excel.Worksheet? sheet = null;
try
{
app = new Excel.Application { Visible = false, DisplayAlerts = false };
book = app.Workbooks.Add();
sheet = (Excel.Worksheet)book.ActiveSheet;
// запись блока одним обращением, а не по ячейке
var data = new object[3, 2]
{
{ "Товар", "Цена" },
{ "Клавиатура", 1990 },
{ "Мышь", 790 }
};
var range = sheet.Range["A1", "B3"];
range.Value2 = data;
range.Columns.AutoFit();
book.SaveAs(@"C:\Temp\interop.xlsx");
}
finally
{
book?.Close(false);
app?.Quit();
if (sheet != null) Marshal.ReleaseComObject(sheet);
if (book != null) Marshal.ReleaseComObject(book);
if (app != null) Marshal.ReleaseComObject(app);
}Две вещи отличают грамотный код с Interop. Первая - запись и чтение диапазонов массивом: каждое обращение к ячейке проходит через границу процессов и стоит дорого, десять тысяч обращений могут занять минуты, а один массив выполняется мгновенно. Вторая - освобождение COM-объектов. Если пропустить их, процесс Excel останется висеть в памяти после закрытия программы. Правила очистки описаны в статье освобождение ресурсов Excel Interop.
Экспорт из базы данных и CSV
Частая задача - выгрузить результат запроса. Прочитайте данные в DataTable (через SqlDataAdapter или Dapper) и передайте таблицу библиотеке. С ClosedXML это одна строка, а значения типов дата и число попадут в Excel в правильном формате, а не строками.
using System.Data;
using Microsoft.Data.SqlClient;
using ClosedXML.Excel;
var table = new DataTable("Клиенты");
using (var connection = new SqlConnection(connectionString))
using (var adapter = new SqlDataAdapter("SELECT Id, Name, CreatedAt FROM Customers", connection))
{
adapter.Fill(table);
}
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add(table, "Клиенты");
sheet.Columns().AdjustToContents();
workbook.SaveAs("customers.xlsx");Если таблица очень большая, от нескольких сотен тысяч строк, лучше записывать данные потоком, например через Open XML SDK или MiniExcel, чтобы не держать весь файл в памяти. Простые выгрузки без стилей иногда удобнее сделать в CSV: он открывается в Excel двойным щелчком и обрабатывается без библиотек, как описано в статье про разбор CSV. Учтите, что русская версия Excel ожидает разделитель точку с запятой и кодировку UTF-8 с BOM.
Полезные приёмы
Номер столбца в буквы (например, 27 в AA) превращается небольшой функцией, готовую реализацию мы разбирали в статье преобразование номера столбца в буквы. Если нужно отдать файл пользователю веба, сохраняйте книгу не на диск, а в MemoryStream и возвращайте как файл с типом application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
// ASP.NET Core minimal API
app.MapGet("/report", () =>
{
using var workbook = new XLWorkbook();
workbook.Worksheets.Add("Отчёт").Cell("A1").Value = "Готово";
var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
return Results.File(stream,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"report.xlsx");
});Для оконных приложений данные из DataGridView экспортируют тем же способом: соберите список объектов или DataTable из источника данных и выгрузите. О создании таких программ рассказано в руководстве про оконные приложения на C#.
Частые ошибки
- Попытка открыть файл, который уже открыт в Excel: возникает
IOException. Закройте книгу или откройте файл потоком с режимом общего чтения. - Путь с обратными слэшами без verbatim-строки: пишите
@"C:\Temp\file.xlsx". - Использование Interop на сервере или в Docker, где Excel не установлен.
- Обращение к ячейкам по одной при записи тысяч строк.
- Расчёт на то, что EPPlus бесплатен для коммерческих проектов: проверяйте лицензию заранее.
- Формат xls в EPPlus и ClosedXML не поддерживается, для него нужен NPOI.
Частые вопросы
Какая библиотека лучше для Excel в C#?
Для большинства задач подойдёт ClosedXML: он бесплатен, прост и покрывает чтение, запись, стили и формулы. EPPlus выбирают за диаграммы и скорость при условии покупки лицензии. NPOI нужен для старых файлов xls.
Нужен ли установленный Excel для работы с xlsx?
Нет, ClosedXML, EPPlus, NPOI и Open XML SDK работают без Office, в том числе в Linux и контейнерах. Excel требуется только для способа через Interop.
Как открыть файл Excel и прочитать данные в C#?
Создайте new XLWorkbook(path), получите лист через Worksheet(1) и перебирайте строки диапазона RangeUsed().RowsUsed(). Значения читайте методами GetString и GetValue<T>.
Как сохранить DataTable или список объектов в Excel?
В ClosedXML используйте workbook.Worksheets.Add(dataTable, "Имя") для таблицы данных и cell.InsertTable(list) для коллекции. В EPPlus - LoadFromDataTable и LoadFromCollection.
Почему после работы через Interop остаётся процесс EXCEL.EXE?
Не освобождены COM-объекты. Закрывайте книгу, вызывайте Quit() и освобождайте каждый объект через Marshal.ReleaseComObject в блоке finally.
Как записать формулу в ячейку?
В ClosedXML присвойте свойству FormulaA1 строку на английском, например "SUM(B2:B10)". В EPPlus используется свойство Formula. Знак равенства в начале не нужен.



