Enterprise applications run on reports. Generating pixel-perfect invoices (PDF) or data-rich financial ledgers (Excel) dynamically from C# code is a highly requested feature. However, choosing the right library requires balancing performance, development speed, and complex commercial licensing.
While EPPlus is historically famous, it requires a commercial license for business use. ClosedXML is the industry-standard, free, open-source wrapper around Microsoft's OpenXML SDK. It makes generating `.xlsx` files incredibly intuitive.
dotnet add package ClosedXML
The best practice is to generate the entire Excel file in physical memory (RAM), avoiding slow disk I/O operations.
using ClosedXML.Excel;
public class ReportController : Controller
{
public IActionResult DownloadUsersExcel()
{
var users = _db.Users.ToList(); // Simulated data
// 1. Create a virtual Workbook
using (var workbook = new XLWorkbook())
{
// 2. Add a Worksheet
var worksheet = workbook.Worksheets.Add("User Report");
// 3. Generate Headers with Styling
worksheet.Cell(1, 1).Value = "User ID";
worksheet.Cell(1, 2).Value = "Full Name";
worksheet.Cell(1, 3).Value = "Email";
var headerRow = worksheet.Row(1);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.AirForceBlue;
headerRow.Style.Font.FontColor = XLColor.White;
// 4. Populate Data Rows
int currentRow = 2;
foreach (var user in users)
{
worksheet.Cell(currentRow, 1).Value = user.Id;
worksheet.Cell(currentRow, 2).Value = user.FullName;
worksheet.Cell(currentRow, 3).Value = user.Email;
currentRow++;
}
// Adjust column widths automatically
worksheet.Columns().AdjustToContents();
// 5. Stream the Workbook to the memory buffer
using (var stream = new MemoryStream())
{
workbook.SaveAs(stream);
var content = stream.ToArray();
// 6. Return standard File Content
return File(content,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"UsersReport.xlsx");
}
}
}
}
PDF generation is notoriously difficult. iText7 is powerful but carries strict commercial licensing obligations. Generating PDFs by converting HTML strings is prone to layout breaks. The modern .NET standard is QuestPDF, a fluent-API library perfect for invoices and structured reports.
dotnet add package QuestPDF
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
public class InvoiceController : Controller
{
public IActionResult DownloadInvoice(int orderId)
{
// Must opt-in to QuestPDF licensing agreement (Community license available)
QuestPDF.Settings.License = LicenseType.Community;
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(12));
// HEADER SECTION
page.Header().Text($"Invoice #{orderId}")
.SemiBold().FontSize(30).FontColor(Colors.Blue.Darken2);
// CONTENT SECTION (Table)
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(50); // Qty
columns.RelativeColumn(); // Item Description
columns.ConstantColumn(100); // Price
});
// Define Table Header
table.Header(header =>
{
header.Cell().Text("Qty").SemiBold();
header.Cell().Text("Product").SemiBold();
header.Cell().AlignRight().Text("Price").SemiBold();
});
// Add a Data Row
table.Cell().Text("2");
table.Cell().Text("Enterprise Developer License");
table.Cell().AlignRight().Text("$450.00");
});
// FOOTER SECTION
page.Footer()
.AlignCenter()
.Text(x => {
x.Span("Page ");
x.CurrentPageNumber();
});
});
});
// 1. Generate directly to a memory stream
byte[] pdfBytes = document.GeneratePdf();
// 2. Return to user
return File(pdfBytes, "application/pdf", $"Invoice_{orderId}.pdf");
}
}
If an Excel sheet has 500,000 rows, generating it inside the Controller will take 2 minutes. The browser HTTP request will simply block, load, and eventually Timeout, displaying a 504 Gateway Error, frustrating the user and tying up server threads.
For massive reports, standard architecture uses Hangfire or Azure Service Bus. The Controller merely returns: "Report generation started. We will email you the download link." A background worker thread builds the PDF/Excel quietly without blocking HTTP requests.
Q: "Why might you choose CSV generation over Excel `.xlsx` generation for exporting simple database tables?"
Architect Answer: "Memory efficiency and dependency reduction. Generating an OpenXML `.xlsx` file requires a massive in-memory representation of worksheet definitions, styles, and rows using libraries like ClosedXML. This causes massive garbage collection pressure. A CSV (Comma-Separated Values) file, however, is a raw text file. We can iterate over our database via an `IAsyncEnumerable`, format the string `Id,Name `, and stream the bytes *directly* into the HTTP Response stream using `StreamWriter`. This uses virtually zero RAM, requires zero third-party NuGets, and exports millions of rows effortlessly."