Данные и базы данных

Работа с Excel в C#: библиотеки и примеры

Обновлено: · 6 мин чтения

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

Работа с Excel в C#: библиотеки и примеры
Содержание
  1. Как работать с Excel из C#: обзор подходов
  2. ClosedXML: запись и чтение
    1. Выгрузка списка объектов и таблицы базы
  3. Форматирование, формулы и даты
  4. EPPlus: создание и чтение xlsx
  5. Interop: управление настоящим Excel
  6. Экспорт из базы данных и CSV
  7. Полезные приёмы
  8. Частые ошибки
  9. Частые вопросы

Как работать с Excel из C#: обзор подходов

Файл xlsx - это архив с XML-документами, поэтому его можно собрать без самого Excel. Именно так работают современные библиотеки. Старый формат xls (до версии 2007) использует бинарную структуру, его поддерживает NPOI. Вторая группа - Interop: код запускает процесс Excel и дирижирует им через COM. Это медленно, требует лицензионного Office на машине и не годится для веб-сервера. Выбор зависит от задачи.

СпособФорматыНужен ExcelЛицензияКогда выбрать
ClosedXMLxlsxнетMIT, бесплатноотчёты, импорт и экспорт, простой API
EPPlusxlsxнетPolyform Noncommercial, для коммерции платнаябогатые стили, диаграммы, большие файлы
NPOIxls, xlsxнетApache 2.0работа со старыми xls
Open XML SDKxlsxнетMITнизкоуровневый контроль и потоковая запись
ExcelDataReader, MiniExcelxls, 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. Знак равенства в начале не нужен.