Generating Excel Files in .NET

Using ClosedXML to make great exports.

Has it really been two months since I posted something? Shit. Ok, how about Excel? Everyone loves Excel! Right?

(crickets)

Whatever, Excel is important and useful.

And I recently had a project for work that required creating Excel files in code. It turned out pretty interesting. Now, I normally don鈥檛 like using Excel as an export format. CSV is way more neutral and easier to write (using CSVHelper) but those files get opened in Excel 99% of the time anyway. In this instance, Excel was a requirement and some formatting needed to be done so CSV wasn鈥檛 going to cut it.

I鈥檝e only done Excel exports from .NET once before this recent project and it was a while ago. I think I used EPPlus, which seems to have changed it鈥檚 licensing terms since then and is now paid. So I set about looking for another library to help and found ClosedXML.

The name is a pun on the OpenXML spec. I love it.

Using ClosedXML is pretty straightforward:

using var wb = new XLWorkbook();
var ws = wb.AddWorksheet();

// Write some data
ws.Cell(1, 1).Value = "Some value";

// Save the file to disk or deliver it from an ASP.NET Core endpoint
wb.SaveAs("SomeFile.xlsx");
wb.Deliver("SomeFile.xlsx");

The documentation is pretty good and covers a lot of use cases and quirks. I want to show you a few things I found.

Keep It Relative

I did this because the app I鈥檓 working on has two exports that can optionally be done in a single file, but I think this is a good practice in general. In the class that creates the exports, I have this:

private (int Row, int Col) _origin = (1, 1);

I鈥檓 keeping an origin point (using a tuple) so that when I write to any cell, I can do it in a way that allows the entire export to be shifted around in the worksheet. The origin is the only fixed point and all cells are defined relative to it:

public void WriteHeaders(IXLWorksheet ws)
{
    ws.Cell(_origin.Row, _origin.Col + 0).Value = "header 1";
    ws.Cell(_origin.Row, _origin.Col + 1).Value = "header 2";
    ws.Cell(_origin.Row, _origin.Col + 2).Value = "header 3";
    ws.Cell(_origin.Row, _origin.Col + 3).Value = "header 4";
    ws.Cell(_origin.Row, _origin.Col + 4).Value = "header 5";
}

This also helps cover up the fact that cells are 1-indexed. I like to treat the worksheet like a big 2d array and using the origin helps a lot with that. There are helper functions provided that can get the first/last used row/column/cell, but I found the origin point method a lot easier to work with.

Writing DateOnly

.NET 6 introduced some new types for DateOnly and TimeOnly. They are incredibly useful for date/time work where you only need one part. For instance, the app I鈥檓 building uses DateOnly a lot in the database model (to map to SQL Server date columns). Time just isn鈥檛 important here.

But ClosedXML falls over itself if you try to write a DateOnly value to a cell. The solution to this is quite simple: convert it to a DateTime:

ws.Cell(row, _origin.Col).Value = someDateOnlyValue.ToDateTime(TimeOnly.MinValue);

ClosedXML can handle DateTime values and Excel is (in this case) smart enough to auto-format that date without a time.

Using Accent Colors

The last thing I want to point out is part of the reason I needed an Excel library in the first place: formatting. Depending on the value I鈥檓 exporting, the cell may need to be highlighted in a certain way. ClosedXML has lots of ways to handle colors, but I wanted this to match an existing file that the client was using. That file used a standard Excel accent color, which ClosedXML provides a nice API for:

var cell = ws.Cell(row, col);

cell.Value = someValue;

if (shouldHighlight)
    cell.Style.Fill.BackgroundColor = XLColor.FromTheme(XLThemeColor.Accent6, 0.4); // Even the tint!

That matches the color and tint from the normal Excel color picker:

Excel color picker showing Accent 6 and 40% tint


A nice short post about something small but helpful. That鈥檚 what I need to write more of. I think I can come up with some more of those. Hopefully I won鈥檛 wait another couple of months before the next one.

Photo by Mika Baumeister on Unsplash