Map spreadsheet headers to typed C# records instead of reading each cell manually.
dotnet add package \ RecursiveAI.OneDocument.Spreadsheets
Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
[SpreadsheetRow] generates the mapping at compile time; keep the record partial.
using RecursiveAI.OneDocument.Spreadsheets; using RecursiveAI.OneDocument.Spreadsheets .Binding; var orders = Spreadsheet.Read<Order>("input.xlsx") .ToList(); foreach (var order in orders) Console.WriteLine( $"{order.Customer}: {order.Total}"); [SpreadsheetRow] public partial record Order( string Customer, int Total);
Ada: 42 Lin: 18
input.xlsx has these cells on its first visible worksheet (named Data); [SpreadsheetRow] selects it by position, not by name:
Customer | Total Ada | 42 Lin | 18
Lenient mapping keeps defaults for bad values and reports defects to OpenContext.Defects (use Spreadsheet.Read<Order>("input.xlsx", context)); a strict row type throws at the first applicable mapping error.
Unzip the example, then dotnet run.
Variations
Enumerate without building a list
using RecursiveAI.OneDocument.Spreadsheets; using RecursiveAI.OneDocument.Spreadsheets .Binding; var orders = Spreadsheet.Read<Order>("input.xlsx"); foreach (var order in orders) Console.WriteLine( $"{order.Customer}: {order.Total}"); [SpreadsheetRow] public partial record Order( string Customer, int Total);
Ada: 42 Lin: 18
Related recipes
The short answer
Mark a partial row type with [SpreadsheetRow], then call Spreadsheet.Read<Order>(path).ToList(). OneDocument generates the mapping code at compile time. The example reads the header names Customer and Total, produces two Order records, and prints their values.
Read<T> is lazy; ToList() is the step that retains all returned objects in memory. If the next operation only needs one row at a time, enumerate the reader directly as shown in the variation. That avoids keeping the entire imported list, though your own downstream code can still accumulate it.
How do I read Excel into a list of objects in C#?
The record in the snippet has a string customer and an integer total, matching the sample workbook’s two headers. It must be partial because the generator adds the ISpreadsheetRow<T> implementation required by the reader. An unmarked ordinary record is not an interchangeable substitute.
The generator ships with the package; the project file in the download pins the published spreadsheets package. You do not need a handwritten cell loop or a reflection-based property assignment step. For this record, its primary constructor receives the matched column values. Classes with a parameterless constructor instead use that constructor and assign writable members.
By default the reader uses the first visible worksheet and treats the first row with a cell as headers. Header matching ignores case and surrounding whitespace. Extra workbook columns are ignored. If a header appears more than once, the first is used and a duplicate-header defect is reported. A row with no value in any mapped column is skipped rather than turned into an empty order.
How do I read data from Excel in C#?
Choose between typed rows and a cell cursor based on what you need. Typed rows are convenient for business data with a known schema. A streaming cell reader is more direct when the job is inspecting arbitrary columns, styles or individual cells rather than constructing a domain model.
For explicit mapping, [SpreadsheetColumn] can set a header name or a fixed one-based column index. Use a header name when columns may move; use an index only when position is part of the file contract. [SpreadsheetRow] can select a named worksheet, disable headers or enable strict reading. Strictness belongs to the type, not to an options argument on Read<T>.
Lenient reading keeps defaults for missing columns or unconvertible values and reports defects to the supplied OpenContext.Defects sink. Strict reading throws at the first applicable error. An empty nullable cell may legitimately be null; a required integer cannot represent that distinction without changing the member type. Choose nullable members when a missing value differs from zero in your application.
Formula cells supply their cached results. The typed reader does not recalculate the workbook. An integer member accepts a numeric value only if it is whole and in range, so a spreadsheet total with a fractional amount does not silently become a rounded integer. Choose decimal for amounts where fractional values are valid, remembering that numeric spreadsheet cells retain Excel’s precision rather than arbitrary decimal precision.
The same typed API also reads CSV and TSV. That does not make those formats equivalent to XLSX: they have no worksheet selection or cell styles.