This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Excelsior is a .NET library for Excel spreadsheet generation (and now deserialization) using a data-driven approach. It uses DocumentFormat.OpenXml for spreadsheet creation and OpenXmlHtml for HTML cell rendering. There is also a Word table generator (WordTableBuilder).
All commands must be run from the repository root.
# Build
dotnet build src --configuration Release
# Run all tests
dotnet test src --configuration Release
# Run a single test project
dotnet test src/Excelsior.Tests
# Run a specific test
dotnet test src/Excelsior.Tests --filter "FullyQualifiedName~UsageTests"CI uses AppVeyor (config at src/appveyor.yml).
When a Verify-based test produces a *.received.{txt,png,xlsx} file, rename it to *.verified.{...} to approve. The first run of any new Verify test always fails — that's expected.
BookBuilder— entry point; sheet registration, build orchestration, and stream/file output. Writes a custom XML metadata part to each workbook (MetadataNamespaceconst) recordingcolumn index → property nameper sheet — this is what the reader uses for round-trip column resolution.Renderer— writes headers and data rows to a sheetSheetBuilder/ISheetBuilder<TModel>— fluent API for per-column write configuration (heading, order, width, render, cell style, format, validation, formulas)ColumnConfig— holds per-column settings;Columnsmanages the ordered listCellStyle/StyleManager— style container + dedup'd OpenXml stylesheet cacheSheetContext— wraps WorksheetPart for cell access and column letter conversionProperty/Properties— reflection-based property discovery with attribute support, including[Split]recursion for flattening nested typesValueRenderer— static config for type-to-string conversion (dates, enums, bool display, whitespace trimming). Settings must be configured via[ModuleInitializer]before anyBookBuilderis constructed; otherwiseThrowIfBookBuilderUsedfires.TemplateSheetBuilder— for emitting empty data-entry templates with explicit column declarations- Attributes:
ColumnAttribute,SplitAttribute,IgnoreAttribute,SheetModelAttribute(inAttributes/) - Word generator:
Word/WordTableBuilder.cs,Word/WordTableRenderer.cs
BookReader— entry point; mirrorsBookBuilder.AddSheet.Convert(stream)throwsReadExceptionon failure;TryConvert(stream)returns aReadResult(implicitbool+ReadError[]). Exception'sErrorsmatches the result'sErrors.SheetReader<TModel>/ISheetReader<TModel>— strong-typed; auto-discovers properties fromTModel, reuses[Column]/[Display]/[DisplayName]attributes for heading resolution.DictionarySheetReader/IDictionarySheetReader— explicit-column path for sheets without a backing model. Each row isIReadOnlyDictionary<string, object?>.SheetParser— header→column resolution (metadata XML first, heading-text fallback), row enumeration, dispatch.CellConverter— primitive parsing inverse ofRenderer.SetCellValue; honoursValueRendererglobal config (BoolDisplay, enum humanizer, TrimWhitespace).ModelActivator<T>— instantiates models viaConstructorInfo.Invoke(parameterless ctor preferred, falls back to longest ctor whose param names match property names).ConstructorInfo.Invokebypasses therequired-members runtime check thatActivator.CreateInstancewould enforce.- Per-column delegate conversion:
sheet.Convert(_ => _.Prop, cell => ...)— the user delegate receives the raw OpenXmlCell.
Excelsior.Tests— main suite, NUnit + VerifyExcelsior.SourceGenerator.Tests— source generator testsStaticSettingsTests— tests for global static settings (date formats, whitespace trimming, enum rendering); separate process to avoidValueRendererglobal-state contaminationSheetRender— .NET Framework 4.8 utility using Excel Interop.RenderExcelopens all.verified.xlsxfiles, renders each sheet's used range to a bitmap, saves as_SheetName.png. Tests are[Explicit](manual run only).Model— shared model classes used by tests (Employee,EmployeeStatus,SampleData)
Referenced as an Analyzer from Excelsior.csproj. Emits GeneratedColumnAttributes so consumers can attach column metadata via partial method declarations rather than runtime attributes — useful for scenarios where the model assembly cannot reference Excelsior.
The readme.md uses MarkdownSnippets with InPlaceOverwrite convention (configured in src/mdsnippets.json). Code samples are pulled from test files via #region SnippetName / #endregion markers. Snippets populate automatically during build (via MarkdownSnippets.MsBuild). When adding new code samples, use <!-- snippet: SnippetName --> / <!-- endSnippet --> references backed by region markers in test code rather than inline code blocks.
- Target framework:
net10.0(withLangVersionset topreview) - Central package management via
src/Directory.Packages.props TreatWarningsAsErrors+EnforceCodeStyleInBuildare both enabled — IDE style rules (e.g.IDE0007 use 'var') fail the build rather than warning. Usevareverywhere; for tests demonstrating an implicit conversion, use a cast (var x = (TargetType)source) rather than declaring the type.- Global usings live in three places: each project's
GlobalUsings.cs, the auto-generated*.GlobalUsings.g.cs(implicit usings), and theProjectDefaultsNuGet package which addsSystem.Text,System.Reflection,System.Diagnostics, etc. Important type aliases:Date = System.DateOnly,Time = System.TimeOnly,Cancel = CancellationToken,CancelSource = CancellationTokenSource. ValueRendererstatic methods (For<T>,ForEnums,BoolDisplay,NullDisplayFor<T>,DisableWhitespaceTrimming,Default*Format) must be called from a[ModuleInitializer]— they throw if invoked after the firstBookBuilderis constructed.- Binary compatibility with Parchment: Parchment consumes
WordTableBuildervia a compilednew WordTableBuilder<T>(data).Build(mainPart)call in itsExcelsiorTableBridge(only the generic-method dispatch is reflection; the ctor call is baked IL). Changing a publicWordTableBuildersignature — including adding an optional constructor parameter — is a binary-breaking change: a Parchment build compiled against the old signature throwsMissingMethodException: WordTableBuilder..ctor(...)at render time until it is rebuilt and republished. Prefer additive fluent methods (e.g.BodyStyle,HeadingParagraphStyle,BodyParagraphStyle) over new/changed constructor parameters so existing Parchment builds keep working. WordTableBuilderemits<w:tbl>directly — it does not go through OpenXmlHtml. OpenXmlHtml renders cell content when a cell isIsHtml, and nothing else; the table structure is built here inWord/WordTableRenderer.cs. A Word table fix landing in OpenXmlHtml therefore does not reach[ExcelsiorTable]output, and the two need separate changes — repeating header rows had to be added toWordContentBuilder.Tables.csthere and toBuildHeaderRowhere. Check the renderer before assuming an OpenXmlHtml bump covers a table-structure change. (The header row is always emitted and carriestblHeader, so a table breaking across a page keeps its heading.)- Word table cell text follows the host's paragraph styles, not the table style. Cells the renderer emits have no
pStyle, so they inherit the document's default paragraph style (Normal), and a paragraph style outranks a table style'srPrper OpenXML precedence — so branding a table's font viaTableGrid/rPralone silently loses toNormal. Style body text either with the run-levelbodyStyle/CellStylecallbacks (plain-text cells only) or, to reachIsHtml/Linkcells too, with the named-paragraph-style methods (HeadingParagraphStyle/BodyParagraphStyle).