diff --git a/src/posts/ExcelAutofiller.md b/src/posts/ExcelAutofiller.md
new file mode 100644
index 0000000..5983c12
--- /dev/null
+++ b/src/posts/ExcelAutofiller.md
@@ -0,0 +1,442 @@
+---
+title: Excel Autofiller
+description: A set of functions for automatically filling excel sheets
+tags: [ Programming, C# ]
+---
+
+These functions will take either an `IEnumerable` or `DataTable` and uses reflection to fill an Excel sheet. This is built for [EPPlus](https://epplussoftware.com/), but it could probably be easily converted to use other libraries.
+
+Attributes on the model can be used to customize the output. The attributes used are the same as the [table generator](/posts/TableAutoGenerator/) so you can use the same model for both.
+
+## How To Use
+
+The model in the `IEnumerable`:
+
+```csharp
+public class Thing
+{
+ // Don't make a column for this property
+ [Display(AutoGenerateField = false)]
+ public int ThingId { get; set; }
+
+ // Use "Thing Name" for the column header instead of "ThingName"
+ [Display(Name = "Thing Name")]
+ public string ThingName { get; set; }
+
+ // Use string.Format with the DataFormatString
+ [DisplayFormat(DataFormatString = "{0:C}")]
+ public decimal Price { get; set; }
+}
+```
+
+Then to fill the Excel sheet:
+
+```csharp
+using ExcelPackage package = new();
+package.Workbook.Worksheets.Add("Data")
+ .FillReport(reportData, "Title");
+```
+
+Other functions can be used to more granularly control how the sheet is filled.
+
+## Code
+
+```csharp
+using OfficeOpenXml.Style;
+using OfficeOpenXml;
+using System.ComponentModel.DataAnnotations;
+using System.Data;
+using System.Reflection;
+
+///
+/// Extension methods for filling an Excel sheet with data
+///
+public static class ExcelFiller
+{
+ #region Fill from IEnumerable
+
+ ///
+ /// Fill an Excel sheet with a title, column headers, and data.
+ ///
+ /// The type of data to fill the sheet with
+ /// The worksheet to fill with data
+ /// The title to put at the top of the worksheet
+ /// The data to use to fill the worksheet
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet FillReport(this ExcelWorksheet worksheet,
+ IEnumerable data, string title)
+ {
+ // Set our printer settings
+ worksheet.PrinterSettings.Orientation = eOrientation.Landscape;
+ //workSheet.PrinterSettings.FitToPage = true;
+ //workSheet.PrinterSettings.FitToHeight = 0;
+ worksheet.PrinterSettings.BottomMargin = .5m;
+ worksheet.PrinterSettings.LeftMargin = .5m;
+ worksheet.PrinterSettings.RightMargin = .5m;
+ worksheet.PrinterSettings.TopMargin = .5m;
+
+ PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns();
+
+ // Set the title
+ using (ExcelRange titleCells = worksheet.Cells[1, 1, 1, propsToDisplay.Count()])
+ {
+ titleCells.Merge = true;
+ titleCells.Value = title;
+ titleCells.Style.Font.Bold = true;
+ titleCells.Style.Font.Size = 12;
+ titleCells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
+ }
+
+ worksheet
+ .InsertDataWithColumnHeaders(data, propsToDisplay, 2)
+ .Cells[worksheet.Dimension.Address].AutoFitColumns();
+
+ return worksheet;
+ }
+
+ ///
+ /// Fill the given excel worksheet with the given data beginning at the given starting row.
+ /// Column headers are generated based on the type
+ ///
+ /// The type of data in the list
+ /// The worksheet to fill
+ /// The data to fill the worksheet with
+ /// The row to begin filling the sheet at. ONE INDEXED.
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertDataWithColumnHeaders(this ExcelWorksheet worksheet,
+ IEnumerable data, int startRow = 1)
+ {
+ PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns();
+
+ return worksheet
+ .InsertColumnHeaders(propsToDisplay, startRow)
+ .InsertData(data, propsToDisplay, startRow + 1);
+ }
+
+ ///
+ /// Fill the given excel worksheet with the given data beginning at the given starting row.
+ /// Column headers are generated based on the type
+ ///
+ /// The type of data in the list
+ /// The worksheet to fill
+ /// The data to fill the worksheet with
+ ///
+ /// The properties of to display
+ ///
+ /// The row to begin filling the sheet at. ONE INDEXED.
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertDataWithColumnHeaders(this ExcelWorksheet worksheet,
+ IEnumerable data, PropertyInfo[] propsToDisplay, int startRow = 1)
+ {
+ return worksheet
+ .InsertColumnHeaders(propsToDisplay, startRow)
+ .InsertData(data, propsToDisplay, startRow + 1);
+ }
+
+ ///
+ /// Fill the given excel worksheet with the given data beginning at the given starting row.
+ /// Column headers are not created.
+ ///
+ /// The type of data in the list
+ /// The worksheet to fill
+ /// The data to fill the worksheet with
+ /// The row to begin filling the sheet at. ONE INDEXED.
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet,
+ IEnumerable data, int startRow = 1)
+ {
+ PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns();
+ return worksheet
+ .InsertData(data, propsToDisplay, startRow);
+ }
+
+ ///
+ /// Fill the given excel worksheet with the given data beginning at the given starting row.
+ /// Column headers are not created.
+ ///
+ /// The type of data in the list
+ /// The worksheet to fill
+ /// The data to fill the worksheet with
+ ///
+ /// The properties of to display
+ ///
+ /// The row to begin filling the sheet at. ONE INDEXED.
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet,
+ IEnumerable data, PropertyInfo[] propsToDisplay, int startRow = 1)
+ {
+ (PropertyInfo Property, Attribute Attribute)[] propAttributes = propsToDisplay
+ .Select(p =>
+ {
+ // Currently [DisplayFormat] is the only attribute used
+ // You can add more with ??
+ var attributes = p.GetCustomAttributes();
+ Attribute attrib = attributes
+ .OfType()
+ .FirstOrDefault();
+ return (p, attrib);
+ })
+ .ToArray();
+
+ // Fill the data
+ int currentRow = startRow;
+ foreach (var row in data)
+ {
+ int currentColumn = 1; // Excel indexes start at 1, not 0
+ foreach ((PropertyInfo property, Attribute attribute) in propAttributes)
+ {
+ var cellValue = property.GetValue(row);
+ ExcelRange cell = worksheet.Cells[currentRow, currentColumn];
+ cell.Value = FormatCellValue(cellValue, attribute);
+
+ currentColumn++;
+ }
+
+ currentRow++;
+ }
+
+ return worksheet;
+ }
+
+ ///
+ /// Fill out the column headers for the type T
+ ///
+ /// The type to use to determine column headers
+ /// The worksheet to add the column headers to
+ /// The row to insert the column headers in
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet,
+ int columnHeaderRow = 1)
+ {
+ PropertyInfo[] propsToDisplay = GetPropertiesForExcelColumns();
+ return worksheet
+ .InsertColumnHeaders(propsToDisplay, columnHeaderRow);
+ }
+
+ ///
+ /// Fill out the column headers for the type
+ ///
+ /// The type to use to determine column headers
+ /// The worksheet to add the column headers to
+ /// The properties that should be displayed
+ /// The row to insert the column headers in
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet,
+ PropertyInfo[] propsToDisplay, int columnHeaderRow = 1)
+ {
+ worksheet.Row(columnHeaderRow).Style.Font.Bold = true;
+ for (int i = 0; i < propsToDisplay.Count(); i++)
+ {
+ // The Property/Attribute array indexes are 0 indexed, but the cells are 1 indexed
+
+ DisplayAttribute[] displayAttributes = (DisplayAttribute[])propsToDisplay[i]
+ .GetCustomAttributes(typeof(DisplayAttribute), false);
+
+ if (displayAttributes.Length > 0 && displayAttributes[0].Name != null)
+ worksheet.Cells[columnHeaderRow, i + 1].Value = displayAttributes[0].Name;
+ else
+ worksheet.Cells[columnHeaderRow, i + 1].Value = propsToDisplay[i].Name;
+ }
+
+ return worksheet;
+ }
+
+ ///
+ /// Get the properties on that should be displayed in Excel sheets
+ ///
+ /// The type to get the properties of
+ public static PropertyInfo[] GetPropertiesForExcelColumns()
+ {
+ return typeof(T).GetProperties()
+ .Where(p =>
+ {
+ DisplayAttribute[] displayAttributes =
+ (DisplayAttribute[])p.GetCustomAttributes(typeof(DisplayAttribute), false);
+ return displayAttributes.Length == 0
+ || displayAttributes[0].GetAutoGenerateField() != false;
+ })
+ .ToArray();
+ }
+
+ ///
+ /// Uses the attribute to determine how to format the value
+ ///
+ /// The value to format
+ /// The attribute to use to format the value
+ /// The formatted value
+ private static object FormatCellValue(object value, Attribute attribute)
+ {
+ // Format negative currency values as "-$123.45" instead of "($123.45)"
+ string cultureString = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
+ System.Globalization.NumberFormatInfo currencyFormat =
+ new System.Globalization.CultureInfo(cultureString).NumberFormat;
+ currencyFormat.CurrencyNegativePattern = 1;
+
+ if (attribute is DisplayFormatAttribute dfAttribute
+ && dfAttribute.DataFormatString != null)
+ return string.Format(currencyFormat, dfAttribute.DataFormatString, value);
+ // AutoFitColumns sets the column too narrow if this is formatted as an actual date
+ // So use a string instead
+ else if (value is DateTime dtValue)
+ return dtValue.ToString("g");
+
+ return value;
+ }
+
+ #endregion
+
+ #region Fill from DataTable
+
+ ///
+ /// Fill an Excel sheet with a title, column headers, and data.
+ ///
+ /// The worksheet to fill with data
+ /// The title to put at the top of the worksheet
+ /// The data to use to fill the worksheet
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet FillReport(this ExcelWorksheet worksheet, DataTable data,
+ string title)
+ {
+ // Format negative currency values as "-$123.45" instead of "($123.45)"
+ string cultureString = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
+ System.Globalization.NumberFormatInfo currencyFormat = new System.Globalization.CultureInfo(cultureString).NumberFormat;
+ currencyFormat.CurrencyNegativePattern = 1;
+
+ //set our printer settings....
+ worksheet.PrinterSettings.Orientation = eOrientation.Landscape;
+ //workSheet.PrinterSettings.FitToPage = true;
+ //workSheet.PrinterSettings.FitToHeight = 0;
+ worksheet.PrinterSettings.BottomMargin = .5m;
+ worksheet.PrinterSettings.LeftMargin = .5m;
+ worksheet.PrinterSettings.RightMargin = .5m;
+ worksheet.PrinterSettings.TopMargin = .5m;
+
+ // Set the report title in the first row
+ using (ExcelRange cells = worksheet.Cells[1, 1, 1, data.Columns.Count])
+ {
+ cells.Merge = true;
+ cells.Value = title;
+ cells.Style.Font.Bold = true;
+ cells.Style.Font.Size = 12;
+ cells.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
+ }
+
+ worksheet
+ .InsertColumnHeaders(data, 2)
+ .InsertData(data, 3);
+
+ worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
+ return worksheet;
+ }
+
+ ///
+ /// Fill out the column headers using the 's column titles
+ ///
+ /// The worksheet to add the column headers to
+ /// The DataTable to get the column titles from
+ /// The row to insert the column headers in
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertColumnHeaders(this ExcelWorksheet worksheet,
+ DataTable data, int columnHeaderRow = 1)
+ {
+ worksheet.Row(columnHeaderRow).Style.Font.Bold = true;
+
+ int currentColumn = 1;
+ foreach (DataColumn dc in data.Columns)
+ {
+ worksheet.Cells[columnHeaderRow, currentColumn].Value = dc.ColumnName;
+ currentColumn++;
+ }
+
+ return worksheet;
+ }
+
+ ///
+ /// Insert data from a DataTable beginning at the given
+ ///
+ /// The worksheet to fill
+ /// The data to fill the worksheet with
+ /// The row to begin filling the worksheet at. ONE INDEXED.
+ ///
+ /// The . This is mutable - returning it only helps with
+ /// chaining.
+ ///
+ public static ExcelWorksheet InsertData(this ExcelWorksheet worksheet, DataTable data,
+ int startRow = 1)
+ {
+ int currentRow = startRow;
+ foreach (DataRow dr in data.Rows)
+ {
+ int currentColumn = 1;
+ foreach (object value in dr.ItemArray)
+ {
+ ExcelRange cell = worksheet.Cells[currentRow, currentColumn];
+ cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Left;
+
+ cell.Value = FormatCellValue(value);
+ currentColumn++;
+ }
+
+ currentRow++;
+ }
+
+ return worksheet;
+ }
+
+ ///
+ /// Handles formatting the given value
+ ///
+ ///
+ /// The value converted to a cleaner type
+ public static object FormatCellValue(object value)
+ {
+ // Some non-text values are stored as strings, convert them to their real type
+ if (value is string strValue)
+ {
+ if (int.TryParse(strValue, out int intValue))
+ return intValue;
+ else if (decimal.TryParse(strValue, out decimal decimalValue))
+ return decimalValue;
+ else if (value.ToString().ToUpper() == true.ToString().ToUpper())
+ return 1;
+ else if (value.ToString().ToUpper() == false.ToString().ToUpper())
+ return 0;
+ else
+ return value;
+ }
+ else if (value is bool boolValue)
+ return boolValue ? 1 : 0;
+ else
+ return value.ToString();
+ }
+
+ #endregion
+}
+```