Exporting SQL Server data to Excel used to mean Office Interop — automating Excel itself from C#. That approach required Office installed on the machine, leaked COM objects if you missed a single ReleaseComObject, and Microsoft has long advised against it on servers, where it is unsupported and unstable. The modern way is to write the .xlsx file directly with a library like ClosedXML: no Excel installation, no COM, safe on servers and in containers, and faster.

This tutorial reads rows from SQL Server and produces a formatted Excel file in about forty lines.
Why not Office Interop anymore?
- Server-side automation of Office is unsupported — Microsoft's own guidance (KB 257757) says not to run Office in services, ASP.NET, or any unattended process. It deadlocks and leaks under load.
- It requires Excel installed on every machine that runs your code — a licensing and deployment burden, and a non-starter in Docker/Linux.
- COM interop is easy to get wrong — one missed release keeps
EXCEL.EXEalive in Task Manager forever.
ClosedXML writes the Open XML .xlsx format directly, so none of that applies. (EPPlus is a fine alternative but is commercially licensed for business use; ClosedXML is MIT.)
Set up the project
dotnet new console -n SqlToExcel
cd SqlToExcel
dotnet add package ClosedXML
dotnet add package Microsoft.Data.SqlClient
Microsoft.Data.SqlClient is the current SQL Server ADO.NET provider — the old System.Data.SqlClient is in maintenance mode.
Read from SQL Server and write the workbook
using ClosedXML.Excel;
using Microsoft.Data.SqlClient;
const string connectionString = "<your connection string>";
using var connection = new SqlConnection(connectionString);
using var command = new SqlCommand(
"SELECT ProductID, Name, ProductNumber, Color, ListPrice " +
"FROM Production.Product ORDER BY ProductID",
connection);
connection.Open();
using var reader = command.ExecuteReader();
using var workbook = new XLWorkbook();
var sheet = workbook.Worksheets.Add("Products");
// header row straight from the result set's columns
for (var col = 0; col < reader.FieldCount; col++)
sheet.Cell(1, col + 1).Value = reader.GetName(col);
sheet.Row(1).Style.Font.Bold = true;
// data rows
var row = 2;
while (reader.Read())
{
for (var col = 0; col < reader.FieldCount; col++)
sheet.Cell(row, col + 1).Value = reader.IsDBNull(col) ? "" : reader.GetValue(col).ToString();
row++;
}
sheet.Column(5).Style.NumberFormat.Format = "#,##0.00";
sheet.Columns().AdjustToContents();
workbook.SaveAs("Products.xlsx");
Console.WriteLine($"Exported {row - 2} products to Products.xlsx");
Running it against the AdventureWorks Production.Product table:
Exported 10 products to Products.xlsx
Open the file and you get a bold header row, formatted prices, and auto-fitted columns. Because the header loop reads reader.GetName(col), the same code exports any query — change the SQL and the spreadsheet follows.
The AdventureWorks sample database used here is available in the SQLSampleDatabase scripts.
Even shorter: insert a DataTable as a table
If you already have a DataTable, ClosedXML inserts it in one call — headers, data, and an Excel table with filter dropdowns included:
var table = new DataTable();
using (var adapter = new SqlDataAdapter(sql, connectionString))
adapter.Fill(table);
using var workbook = new XLWorkbook();
workbook.Worksheets.Add(table, "Products");
workbook.SaveAs("Products.xlsx");
Returning the file from a Web API
The same workbook can be streamed from an ASP.NET Core endpoint instead of saved to disk:
[HttpGet("export")]
public IActionResult Export()
{
using var workbook = BuildWorkbook(); // the code above, returning XLWorkbook
var stream = new MemoryStream();
workbook.SaveAs(stream);
stream.Position = 0;
return File(stream,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Products.xlsx");
}
The browser downloads Products.xlsx like any other file — more on returning files in
returning a file using FileResult.
Wrap-up
- Never automate Excel via Interop on a server — it's unsupported and fragile.
- ClosedXML (MIT-licensed) writes real
.xlsxfiles with styling, formulas, and tables, and runs anywhere .NET runs — Windows, Linux, containers. - The reader-loop pattern exports any query; the
DataTableoverload is a one-liner; aMemoryStreamturns it into an API download.
Comments (0)
No comments yet — be the first to share your thoughts.