OfficeIMO.Excel 3.5.2

Prefix Reserved
dotnet add package OfficeIMO.Excel --version 3.5.2
                    
NuGet\Install-Package OfficeIMO.Excel -Version 3.5.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OfficeIMO.Excel" Version="3.5.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OfficeIMO.Excel" Version="3.5.2" />
                    
Directory.Packages.props
<PackageReference Include="OfficeIMO.Excel" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OfficeIMO.Excel --version 3.5.2
                    
#r "nuget: OfficeIMO.Excel, 3.5.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OfficeIMO.Excel@3.5.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OfficeIMO.Excel&version=3.5.2
                    
Install as a Cake Addin
#tool nuget:?package=OfficeIMO.Excel&version=3.5.2
                    
Install as a Cake Tool

OfficeIMO.Excel - Excel workbooks for .NET

nuget version nuget downloads

OfficeIMO.Excel is the main Excel package in the OfficeIMO family. It creates, edits, reads, converts, and saves .xlsx workbooks without COM automation and without Microsoft Excel installed. It also opens BIFF8 .xls and BIFF12 .xlsb workbooks, projects supported content into the normal OfficeIMO model, and provides first-party native writer subsets with explicit preservation and loss diagnostics.

Bounded native inspection

ExcelLoadOptions.AllowBinaryProjection = false accepts XML workbook packages and rejects XLS/XLSB before their secondary payload/model projection. The default keeps existing binary import behavior. The inspection snapshot retains saved values, authored formulas, comments and worksheet state without evaluating formulas.

InspectEmbeddedPayloads(options, cancellationToken) inventories inert package, OLE and ActiveX descriptors and provides selected bounded OpenRead(ordinal) streams. OpenEmbeddedPayload(id, maximumBytes, cancellationToken) is the direct single-payload convenience API. Keep the workbook open while requesting reads; returned complete read-only streams are caller-owned and survive workbook disposal. The Core payload contract defines count, member, aggregate and cumulative work limits, unknown lengths and optional hashing. InspectVbaProjectMetadata() inventories the part and signature relationship without reading binary module content, with ModuleInspectionRequested = false.

If OfficeIMO saves you time, please consider supporting the work through GitHub Sponsors or PayPal. PowerShell users should use PSWriteOffice for the PowerShell-facing experience.

Install

dotnet add package OfficeIMO.Excel

Quick start

using OfficeIMO.Excel;

using var document = ExcelDocument.Create("report.xlsx");
var sheet = document.AddWorksheet("Data");

sheet.CellValue(1, 1, "Name");
sheet.CellValue(1, 2, "Value");
sheet.CellValue(2, 1, "Alpha");
sheet.CellValue(2, 2, 42);
sheet.AddTable("A1:B2", hasHeader: true, name: "DataTable", style: ExcelTableStyle.TableStyleMedium9);
sheet.AutoFitColumns();

document.Save();

When a workbook comes from an untrusted source, pass the bounded load profile before parsing it:

using var incoming = ExcelDocument.Load("upload.xlsx", ExcelLoadOptions.UntrustedDefaults);

This profile rejects macros, embedded payloads, ActiveX, and external relationships. Ordinary load options retain compatibility with workbooks containing those parts; PackageSecurity can be set explicitly for a different policy.

Retain original-package evidence without a second file read:

using OfficeIMO;
using OfficeIMO.Excel;

using var workbook = ExcelDocument.Load("review.xlsx", new ExcelLoadOptions {
    AccessMode = DocumentAccessMode.ReadOnly,
    PackageSecurity = new OfficePackageSecurityOptions { MaxPackageBytes = 64L * 1024 * 1024 },
    CaptureSourcePackageSecurity = true
});
Console.WriteLine(workbook.SourcePackageSecurityReport?.ContainerKind);
Console.WriteLine(workbook.SourcePackageSecurityScope);
var loadedSafety = workbook.InspectLoadedContentSafety();

Capture uses the loader's source bytes and retains their physical scope, including an encrypted outer container where applicable. Later model edits do not rewrite that report. Loaded-content safety inspection examines the current workbook without reopening, saving, recalculating formulas or refreshing external links. Structural findings and saved values remain evidence; they do not verify signature validity or establish a safe workbook. Source capture is opt-in, and load/import budgets still apply.

For ordinary workbook work, use ExcelDocument.Create(...) or ExcelDocument.Load(...), edit the same document through its sheets, then call Save(). Use ExcelDocument.OpenDataReader(...) when you only need forward-only rows. Formula calculation, dynamic arrays, and pivot materialization are operations on that workbook model when a report needs them; they are not alternate ways to open a workbook.

Set selected cell edges with sheet.CellBorder(1, 1, ExcelCellBorders.Bottom, ExcelBorderStyle.Thick, "FF0000") or the cell wrapper's SetBorder(...). The other edges, font, fill and number format remain intact. ExcelBorderStyle.None explicitly clears the selected edges; the existing CellBorder(row, column, style, color) overload applies to all four sides. Cell coordinates are one-based.

AsFluent() wraps the same ExcelDocument; it does not create a separate workbook model. Call End() when direct worksheet APIs are more convenient:

using var document = ExcelDocument.Create("report.xlsx");

document.AsFluent()
    .Sheet("Data", sheet => sheet
        .Cell(1, 1, "Name")
        .Cell(1, 2, "Value")
        .Cell(2, 1, "Alpha")
        .Cell(2, 2, 42))
    .End();

document.Save();

What it does

  • Creates and edits workbooks, worksheets, cells, ranges, tables, styles, hyperlinks, formulas, names, comments, images, charts, filters, and page setup.
  • Reads tabular values through the forward-only ExcelDocument.OpenDataReader(...) API and typed ExcelSheet.RowsAs<T>(...) helpers. Numeric cells and formula caches retain stored double precision across range, streaming, and typed reads; numeric-looking text retains its text type.
  • Edits loaded workbooks through the normal worksheet, cell, range, table, and fluent authoring APIs.
  • Handles practical workbook hygiene such as table/filter conflicts, safe table names, deterministic save order, and feature inspection.
  • Renders supported numeric display formats consistently in PDF and image export, including mixed fractions that retain whole values beyond Int64, optional decimals, scientific mantissa precision and exponent sign/case/padding, and red negative-number sections. Quoted or escaped exponent letters and decimal points remain literals. Numeric-looking text retains its type, contents, and font color. Other number-format colors are not projected.
  • Renders common date/time and elapsed formats through the same formatter in PDF and image export. Custom patterns retain date separators, quoted or escaped labels, 12-hour markers, elapsed unit padding, and selected negative or conditional sections. Fractional seconds support one to seven digits and round across component boundaries. Month and weekday names use invariant English; locale directives and alternate calendars are not localized.
  • Applies optional shared package-security policy before parsing Open XML, XLSB, or compound XLS files.
  • Includes parallel execution controls for heavy export and autofit workloads while serializing the Open XML mutation phase safely.

Performance evidence

For reports written cell by cell, group formatting in sheet.Batch(target => { ... }). Repeated styles share indexed lookups, and ordinary formatting writes the stylesheet once when the batch ends, including gradient fills, named-style definitions, conditional-format styles and legacy XLS style projection. Batches can edit other worksheets in the same workbook, including cell values and formulas. Transactional operations retain the snapshot flushes needed for rollback. Layout and report metadata changes, including merges, filters, comments, table settings, view options, protection and text replacement, reach the package at document.Save(...). Setters with an explicit save argument retain that option; use save: false in repeated outline calls. For many threaded comments, use AddThreadedComments(...) to write comments and person metadata together.

Automatic date and duration formatting uses the current named-style definition, including after a style is redefined on another worksheet. Existing cells with their own formatting overrides retain those overrides.

Header lookups and text searches share their cache state across wrappers for the same worksheet, including when SheetCachingEnabled is false. Header names follow the current number format and date-reading options; custom value converters run for each requested header read.

WrapCells, CellWrapText and automatic wrapping for multiline values preserve existing alignment and other formatting. Styled hyperlinks preserve the cell's alignment, protection and other font details while applying blue text and an underline. ExcelSaveOptions.ValidateOpenXml validates the encoded package before writing the destination, including when a fast package writer is used. Fast writers favor write throughput over ZIP size; set DisableFastPackageWriter = true when the standard packaging path is preferred.

OfficeIMO.Excel is optimized for fast tabular reads and writes, but it is not only a streaming data pipe. The same first-party model authors and edits styles, tables, formulas, charts, pivots, conditional formatting, validation, images, templates, protection, print settings, headers and footers, and both .xlsx and the supported legacy .xls subset.

Performance claims use validated outputs and record the workload, package versions, runtime, operating system, processor, warm-up, iterations, allocations, and source provenance. Windows, Linux, and macOS remain separate evidence lanes; missing platforms stay visible rather than being inferred from another operating system.

Use the benchmark website for dated comparison snapshots and their recorded environments. The benchmark harness documents reproducible local runs, workload validation, allocation evidence, and data publication. Benchmark-only libraries remain isolated from the OfficeIMO.Excel runtime package.

The opt-in tabular comparison suite covers typed and raw reads, shared strings, native workbook writes, real-data files, CSV, Arrow, and ADO.NET consumers. It validates every field and records API differences separately from equivalent-work comparisons.

Examples

The quick start covers the smallest workbook. These examples show common read, write, reporting, and automation workflows that belong in OfficeIMO.Excel.

Read rows by header

using var reader = ExcelDocument.OpenDataReader("input.xlsx", new ExcelReadOptions {
    SheetName = "Data"
});

while (reader.Read()) {
    Console.WriteLine(reader["Name"]);
}

Work with legacy XLS workbooks

using var document = ExcelDocument.Load("legacy.xls");
ExcelFeatureReport report = document.InspectFeatures();

document.Save("converted.xlsx");
document.Save("native-copy.xls");

ExcelDocument.Convert("legacy.xls", "converted.xlsx");
ExcelDocument.Convert("openxml.xlsx", "native-copy.xls");

BIFF8 .xls files load through the normal ExcelDocument.Load entry point. Supported cells, formulas, styles, names, comments, filters, validations, conditional formatting, layout, protection metadata, document properties, images, drawings, tables, and chart sheets project into the normal OfficeIMO model. Unsupported sheet kinds, VBA, embedded OLE content, signatures, and unprojected BIFF records are reported through the legacy import diagnostics instead of being silently dropped.

Native .xls save uses the same Save("*.xls") path as other OfficeIMO saves. Imported BIFF8 workbooks retain their native chart sheets, VBA streams, embedded content and other compound streams during supported cell and worksheet edits. The preserving writer supports new and deleted cells, value/type changes, encodable formulas, rich text and styles, worksheet rename/add/remove/reorder, and save-time calculation policies. Plain and supported encrypted sources use the same path, with plain or password-to-open output. Worksheet structure edits keep native references, local-name scopes, chart carriers and VBA document-host identities coherent. Signed sources and unsupported feature edits remain guarded. See the preserving profile for format limits, retained-content boundaries and native Excel evidence. When a workbook contains a feature outside the supported BIFF8 writer subset, OfficeIMO throws a preflight error with the unsupported feature name so the caller can save as .xlsx, remove the feature, or choose a different workflow. ExcelDocument.Convert(...) uses those same load and save paths and blocks legacy sources with unsupported or preserve-only content by default. Set LossPolicy to OfficeConversionLossPolicy.Allow on conversion or save options only after reviewing that loss. See XLS and XLSX compatibility for the current capability matrix and safety contract. Use the migration guide for canonical API replacements.

XML Spreadsheet 2003 data interchange

Use the explicit XML APIs for standalone XML Spreadsheet 2003 files. They share the normal editable workbook model; Load and Save retain their existing XLSX/XLSM, XLS and XLSB routes.

using OfficeIMO.Excel;

var imported = ExcelDocument.ImportSpreadsheetXml("archive.xml");
using var workbook = imported.Value;
foreach (var diagnostic in imported.Report.FidelityDiagnostics)
    Console.WriteLine(diagnostic.Message);
imported.Report.RequireNoLoss(); // Optional strict admission, before editing or saving.
workbook.Save("editable.xlsx");

var exported = workbook.ExportSpreadsheetXml();
exported.Report.RequireNoLoss(); // Review omissions before publishing the XML bytes.
File.WriteAllBytes("exchange.xml", exported.Value);

Import accepts paths, bytes and caller-owned streams. Stream import starts at the current position, advances it and leaves the stream open. Export returns UTF-8 XML bytes, supports read-only workbooks and retains the workbook's file association. The result's workbook is caller-owned, including when strict admission rejects its report.

The workbook profile carries multiple worksheets, sparse coordinates, typed cells, ordinary formulas with cached values, date serials and number formats. It maps inherited fonts, pattern fills, borders, alignment and cell protection into the existing workbook style model. It also carries row heights, column widths and styles, hidden rows/columns/sheets, merged cells, scoped names, comments, hyperlinks, ordinary print settings and frozen panes. The supported rich-text subset includes bold, italic, underline, strike, subscript/superscript and font name, size and colour. The shared syntax owner converts ordinary R1C1 formulas to A1 on import and back on export without calculating them. Dates use the workbook's 1900 or 1904 epoch.

Review the operation's diagnostics before accepting conversion. Charts, pivots, modern Excel tables, conditional formatting and other unimplemented metadata are reported as omissions. Theme colours are resolved to RGB; theme identity is reported as lost. Native Excel can quantize XML font colours to its legacy palette even when the XML retains the requested RGB. This is not a general pixel-fidelity or complete XML Spreadsheet feature claim.

Column dimensions in XML are points; Open XML stores font-dependent character widths. Installed default-font metrics drive the conversion. If that font is unavailable, the report identifies the seven-pixel approximation. Set ExcelSpreadsheetXmlOptions.DefaultFontMaximumDigitWidthPixels when the target font's measured maximum digit width at 96 DPI is known. The native fixture cohort uses its observed eight-pixel width; that observation is not a universal font default.

Native XML Spreadsheet coordinates are limited to 65,536 rows and 256 columns. An invalid or overlapping merge is rejected before returning a workbook. Export rejects covered merged-cell data that the target format would discard. ExcelSpreadsheetXmlOptions bounds bytes, stored cells, worksheets, styles, XML elements and depth, and accepts cancellation. DTDs and external XML entity resolution are disabled. The fixture corpus distinguishes native producer, native consumer and managed round-trip evidence.

Exchange XML tables

ExcelSheet.FromXml inserts an XML table through the existing DataTable path; ToXml exports an explicit bounded A1 range using cached cell values. This is separate from XML Spreadsheet workbook conversion. The default schema is Table containing Row elements with repeated Data children. Text stays literal, so leading zeroes and formula-looking strings are retained.

using var workbook = ExcelDocument.Create();
var sheet = workbook.AddWorksheet("Orders");
sheet.FromXml("<Table><Row><Data>00127</Data><Data>Ready</Data></Row></Table>");
string xml = sheet.ToXml("A1:B1");

For named elements or row attributes, supply ordered ExcelXmlTableColumn mappings through ExcelXmlTableOptions.Columns. Configure the root/row names and namespace explicitly. Each column selects text, finite number, XML Boolean or date/time conversion. Unmapped fields are outside the selected table; repeated matches and nested scalar markup are rejected. Missing named fields remain blank. Positional export uses xsi:nil to distinguish missing cells from empty strings. The complete XML is parsed and converted before insertion; existing worksheet insertion cancellation semantics apply while cells are written. Caller streams remain open. Byte, row, column, rectangular-cell, element and depth limits apply; external schemas, DTDs and XML entities are never resolved.

Import additional legacy spreadsheet formats

The OfficeIMO.Excel package also contains an explicit, read-only importer for selected Lotus 1-2-3, Quattro Pro, Multiplan, Microsoft Works, SYLK (.slk), and DIF (.dif) sources. No additional package is required, and these formats are only processed when the application calls LegacySpreadsheetImporter or explicitly registers the corresponding Reader handler.

using OfficeIMO;
using OfficeIMO.Excel.Legacy;

using LegacySpreadsheetImportResult imported = LegacySpreadsheetImporter.Import("archive.wk1");
Console.WriteLine(imported.Report.Quality);
foreach (LegacySpreadsheetCellContent cell in imported.Cells) {
    Console.WriteLine($"{cell.SheetName}!R{cell.Row}C{cell.Column}: {cell.Formula ?? cell.CachedValue}");
}
foreach (OfficeCompatibilityFinding finding in imported.Report.Findings) {
    Console.WriteLine($"{finding.Code}: {finding.Message}");
}

imported.Value.Save("archive.xlsx");

The importer never saves back to these source formats, executes macros, activates embedded objects, or resolves and refreshes external links. Each result identifies structured or salvage recovery and reports feature-level loss. Existing Excel converter packages can export the returned workbook to ODS, CSV, HTML, or PDF.

Profile coverage
Family/profile Quality Recovered today Explicit boundary
Lotus 1-2-3 WK1 0x0406 record streams Structured empty workbooks, cells, labels, numbers, finite formula caches, bounded RPN formula translation, names, selected number formats, alignment, and chart metadata WK1 is projected as one source sheet; unsupported formula tokens retain the cache with a diagnostic
Lotus later record streams with BOF 0x1000, 0x1002, or 0x1005 Structured multiple sheets, sheet names, cell addresses, extended and compact numbers, comments, alignment, and bounded RPN formulas with sheet references other BOF generations remain salvage; styles, names, charts, and application data after EOF are not reconstructed; unsupported formulas retain available caches
Quattro Pro DOS WQ1/WQ2 record streams Structured sheet identifiers, cells, finite cached values, names, alignment, and chart metadata WQ formulas retain cached values with a diagnostic
Quattro Pro WB1 0x1001 Structured sheets and names, cell addresses, labels, numbers, caches, and bounded formulas with local or internal-sheet cell/range references WB2/WB3 and QPW remain salvage; named/external/collection formula references, advanced formatting, and charts are not reconstructed
Microsoft Works DOS WKS 0x0404 record streams Structured cells, cached values, safe formulas, names, selected number formats, alignment, and chart metadata DOS profile only
Microsoft Works Windows WKS (0x00FF BOF, 0x0404 payload) Structured cell addresses, labels and text continuations, single/double-precision numbers, caches, and bounded formulas compound/XLR profiles remain salvage; Windows styles, print/database metadata, and charts are not reconstructed
Later Lotus 123, Quattro QPW, and Works XLR/binary profiles Salvage bounded text/tabular runs and compound-content safety inventory where applicable workbook structure, formulas, names, comments, advanced formatting, and charts are reported as unavailable
Microsoft Multiplan DOS 1-3 Salvage bounded text and tabular runs cell zones, formulas, names, formats, comments, and charts are not yet semantically decoded
SYLK sylk-stored-values Structured one worksheet, explicit blanks, sticky cell coordinates, literal quote runs, finite numbers, booleans, and valid stored formula/matrix/table values formulas and shared/matrix/table behavior are omitted without evaluation; missing caches become blank cells with loss diagnostics; standard error markers become literal text with a diagnostic; styles, number formats, widths, names and other unsupported records are reported as omitted; character escapes other than line breaks and the historical SCALC3 string dialect are rejected
DIF dif-row-values Structured source row order, quoted/multiline text, finite numbers, booleans, and table-name metadata DIF carries values rather than formulas or formatting; NA/ERROR become literal text with a diagnostic; unsupported topics and dimension mismatches are reported; transposed header counts never transpose the data

The later Lotus, WB1, and Windows WKS readers use separate record and formula layouts. RequireStructured = true accepts these selected profiles and rejects salvage-only generations. Independent upstream Lotus 98, Quattro WB1, and Works Windows fixtures exercise import and XLSX save/reopen; the older later-Lotus BOF variants also have specification-built numeric/formula checks. Text outside the qualified ASCII profile is replaced with a diagnostic. Review Report.Findings before using a converted workbook for calculations.

Structured WK-derived profiles accept a valid BOF/EOF workbook with no cells and currently require ASCII text. Formula translation is allow-listed, bounded, charged against the import-wide text budget, and never evaluates the source expression. Unsupported tokens retain only a finite cached value with a loss diagnostic. Structured means the record stream passed the profile grammar, not that conversion is lossless; inspect Report.Findings, or call imported.RequireNoLoss() when salvage recovery, inert content, or any known approximation must fail the workflow.

SYLK and DIF use the same file, byte-array, and caller-owned stream overloads. Detection checks the content signature; an extension alone is insufficient. Stream import starts at the current position and leaves the stream open. Input bytes, physical records, cell count, and retained text are bounded by LegacySpreadsheetImportOptions.Limits; oversized cells, non-finite numbers, duplicate SYLK cell definitions, missing terminators, and data after a terminator are rejected. Source dimensions do not allocate blank grids. SYLK records accept at most 64 fields; omission reports retain at most 64 distinct record/topic kinds and an overflow notice.

Text without a BOM uses Windows-1252. Set TextEncoding explicitly for a different source encoding; a Unicode BOM takes precedence, and invalid encoded text fails instead of being silently replaced. Quoted formula-looking text remains literal in the returned workbook. Numeric date values remain numbers because source formatting is omitted.

using OfficeIMO;
using OfficeIMO.Excel.Legacy;

using var imported = LegacySpreadsheetImporter.Import("archive.slk", new LegacySpreadsheetImportOptions {
    RequireStructured = true,
    TextEncoding = System.Text.Encoding.UTF8,
    Limits = new OfficeLegacyImportLimits { MaxInputBytes = 16 * 1024 * 1024, MaxItems = 50_000 }
});
foreach (OfficeCompatibilityFinding finding in imported.Report.Findings) {
    Console.WriteLine($"{finding.Code}: {finding.Message}");
}
imported.Value.Save("archive.xlsx"); // The same API accepts archive.dif.

The interchange fixtures include independently exported LibreOffice files. Their coverage is a bounded import profile, not a claim of every historical producer dialect.

Work with XLSB workbooks

using OfficeIMO.Excel;
using OfficeIMO.Excel.Xlsb;

using var document = ExcelDocument.Load(
    "source.xlsb",
    new ExcelLoadOptions {
        XlsbImportOptions = new XlsbImportOptions { MaxCells = 2_000_000 }
    });

document["Data"].CellValue(2, 2, 1250m);
document.Save("edited.xlsb");

ExcelDocument.Convert("source.xlsb", "editable.xlsx");

XLSB detection uses package content rather than trusting the extension. The importer projects supported values, formulas, styles, dates, geometry, views, merges, names, and hyperlinks while retaining unknown BIFF12 records and unmodified package parts. Supported cell edits use a native preservation-aware rewrite. Unsupported mutations and save-time transforms fail before output is written, so .xlsx bytes are never disguised as .xlsb.

Chart-sheet drawing/chart parts and linked VBA project parts are projected through their original relationships without executing macros or following external links. Native cell, formula, style and worksheet rename/add/remove/reorder operations retain chart and VBA carriers. Structure edits update references, scoped names, chart formulas and VBA document-host identities. Shared formula groups and fixed array formulas retain their native semantics; save-time calculation options can evaluate supported formulas, clear caches or request native recalculation. Editing the retained artwork or other unsupported package content remains guarded; use macro-compatible Open XML output for those edits. Unprojected chart-sheet view, protection and print records remain visible in XlsbPreservedRecords; native preservation does not imply lossless conversion. MaxRelatedParts and MaxRelationshipDepth bound imported chart/VBA graphs in addition to the existing decompressed-part and package budgets.

New XLSB workbooks use inline text cells by default. To deduplicate repeated text across worksheets, save with new ExcelSaveOptions { XlsbUseSharedStrings = true }. Set the option to false to select inline strings explicitly. Shared-string storage retains each distinct text value in memory during save. Leave the option unset when saving an imported XLSB workbook: its original string storage and preserved records are retained. Explicit storage changes on imported XLSB workbooks, or on another output format, are rejected before writing the destination.

For untrusted files, capability preflight, macro and embedded-payload handling, and DOC/XLS/XLSB loss policies, see the Word and Excel interoperability guide.

Stream workbook rows

List worksheet names without decoding worksheet cells, shared strings, or styles:

IReadOnlyList<string> sheetNames = ExcelDocument.GetSheetNames(
    "input.xlsx",
    new ExcelReadOptions {
        MaxWorksheets = 256,
        MaxMetadataPartBytes = 4 * 1024 * 1024
    });

GetSheetNames supports XLSX, XLSM, XLTX, XLTM, XLAM, XLSB, and BIFF5/BIFF8 XLS. It returns readable worksheets in workbook order and applies MaxInputBytes, MaxWorksheets, MaxMetadataPartBytes, and the configured cancellation token before worksheet data is opened.

Modern XML read APIs report configured input, metadata, shared-string, dense-range, and data-reader buffering ceilings as OfficeInspectionResourceLimitException. Its Owner is excel; LimitName and Maximum identify the effective native ceiling. Malformed package content retains its existing error contract. The bounded decrypted-package check uses the canonical encryption owner.

URI reads and loads apply native input and package-security ceilings alongside ExcelHttpLoadOptions.MaxBytes. HTTP and read-input byte failures identify the binding MaxBytes or MaxInputBytes property and effective maximum under the excel owner. A binding PackageSecurity.MaxPackageBytes policy retains the Core OfficePackageSecurityException and PackageSize rule, including ties with the input ceiling. Transport failures retain their existing exception identity.

Input and metadata admission applies while opening package input. Readers over an already-loaded mutable model do not reapply those checks to its earlier load. MaxRangeCells bounds dense range APIs; it does not cap the total cells delivered by row streaming. MaxXmlDataReaderBufferedCharacters applies to the XML data-reader buffer, rather than every workbook extraction path or total process memory.

using OfficeIMO.Excel;

using var reader = ExcelDocument.OpenDataReader("input.xlsx", new ExcelReadOptions {
    SheetIndex = 0,
    A1Range = "A1:B1000",
    NumericAsDecimal = true
});
Console.WriteLine(reader.CurrentSheetName);
while (reader.Read()) {
    string name = reader.GetString(reader.GetOrdinal("Full Name"));
    decimal value = reader.GetDecimal(reader.GetOrdinal("Value"));
    Console.WriteLine($"{name}: {value}");
}

XLSX numeric cells and cached numeric formula results use invariant number notation, so 1.5 remains 1.5 when ExcelReadOptions.Culture is de-DE. The configured culture remains the fallback for legacy localized numeric text and controls conversions from string cells. CellValueConverter receives the original decoded text and the configured culture before built-in conversion.

Worksheet rows and cells come from the worksheet's direct sheetData content. Inline and shared strings include visible text and rich runs. Extension payloads and phonetic guidance do not add cells, shift shared-string indexes or append text.

OpenDataReaderAsync reads local files and the remaining bytes of a readable stream using asynchronous I/O, bounded by MaxInputBytes. Workbook validation and worksheet discovery then run synchronously. Caller-owned streams stay open, and seekable streams return to their original position after input is read. The opening cancellation token and the token in ExcelReadOptions remain active for the returned reader.

On .NET 8 and later, ReadAsync, NextResultAsync, and RowsAsAsync<T> propagate cancellation through the native workbook reader:

using OfficeIMO.Data;

await using var reader = await ExcelDocument.OpenDataReaderAsync("input.xlsx", new ExcelReadOptions {
    SheetName = "Data"
}, cancellationToken);

await foreach (InvoiceRow row in reader.RowsAsAsync<InvoiceRow>(cancellationToken)) {
    await ProcessAsync(row, cancellationToken);
}

Password-to-open output

SaveEncrypted(path, password) selects XLSX/XLSM, XLSB or XLS from the destination extension. For a stream, select the physical format explicitly:

using var document = ExcelDocument.Create();
document.AddWorksheet("Data").CellValue(1, 1, "Protected workbook");
document.SaveEncrypted("protected.xlsb", password);
using var output = new MemoryStream();
document.SaveEncrypted(output, password, ExcelFileFormat.Xls);

XLSX/XLSM and XLSB use Agile Office package encryption. XLSB contains a native BIFF12 package. XLS uses classic BIFF8 RC4/MD5 with a password of 1–15 UTF-16 code units. Imported protected auxiliary entries retain RC4 CryptoAPI on encrypted saves, including password rotation, instead of becoming clear compound streams. Explicit plaintext output still restores those entries. These legacy algorithms provide weak protection; choose XLSX or XLSB for modern password-to-open encryption. XLS encryption does not encrypt VBA project streams or all compound-file metadata, as defined by the format. Retained pivot cache and revision BIFF streams are encrypted with their own stream positions and record-specific plaintext fields. Supported encrypted-source metadata is decoded for preservation; passwords are used for the operation and are not retained by the document. An explicit encrypted save can rotate the password.

Encryption retains each native encoder's feature and loss-policy limits. MaxInMemoryPackageBytes also bounds native BIFF serialization and XLSB intermediate parts before their output buffers grow; it is not a total process memory budget. The included XLS/XLSB worksheet controls and imported chart/macro preservation controls open in native Excel without repair with macros disabled. This qualifies the documented edit profiles; macro execution and arbitrary workbook features remain separate. Password-to-modify and worksheet/workbook protection are separate from password-to-open encryption.

After encrypting the associated file or stream, automatic persistence is disabled so disposal cannot replace the encrypted output with plaintext. No password is retained. Use SaveEncrypted explicitly for later changes; an ordinary explicit Save writes an unencrypted file. Exporting to a different stream keeps the original destination's persistence policy.

OpenEncryptedDataReader opens Office Open XML workbooks protected by Agile encryption from a file, byte array, or the remaining bytes of a readable stream. The decrypted package determines the XLSX or XLSB format. Package integrity is always verified. Legacy XLS password encryption uses LoadEncrypted.

using var reader = ExcelDocument.OpenEncryptedDataReader(
    "protected.xlsx", password, new ExcelReadOptions {
        MaxInputBytes = 64L * 1024 * 1024
    });
while (reader.Read()) {
    Console.WriteLine(reader.GetValue(0));
}

Opening buffers the complete encrypted input and decrypted package. Each has its own size check against MaxInputBytes; the limit is not a combined memory budget. The ordinary package-part, row and schema limits also apply. Options are copied when opening starts. Caller-owned streams remain open, and seekable positions are restored after input is read, including on failure.

OpenEncryptedDataReaderAsync supports local paths and streams. It uses asynchronous input, then performs cancellable decryption, integrity verification and workbook discovery synchronously. Its opening cancellation token and the token in ExcelReadOptions remain active for the returned reader.

ExcelDocument.OpenDataReader returns an ExcelWorkbookDataReader, the package-owned read-only entry point for XLSX, XLSM, XLTX, XLTM, XLAM, XLSB, and BIFF8 XLS. It discovers used ranges and exposes additional worksheets through NextResult(). Select one worksheet with SheetName or the zero-based SheetIndex, and select an explicit range with A1Range. CurrentSheetName and CurrentSheetIndex identify the active workbook sheet; CurrentResultIndex identifies its position in the selected results. Legacy XLS is projected through the package's existing first-party reader; use ExcelDocument.Load when the workbook must be inspected, edited, or saved again. CSV provides the same typed and ordered-parallel row-mapping contracts through the separate OfficeIMO.CSV package.

For indexed native XLSX reads, OpenDataReader qualifies the complete selected worksheet before returning. Its first shared-string cell, when present, causes the opening scan to load the complete workbook shared-string table, checking XML and item/count/character limits, including later unused items. This occurs before the caller's first Read or value getter, even with HasHeaderRow = false. First-row latency includes this opening work; the reader exposes no option to replace it with XML count attributes or a partial shared-string table load. See the tabular lifecycle measurements for the first-row and full-scan evidence boundaries.

The XML streaming fallback completes a row before returning a cell value, so scalar and bulk getters agree when cell coordinates recur or arrive out of order. ExcelReadOptions.MaxXmlDataReaderBufferedCharacters bounds this materialization to 32 Mi characters per physical row by default. Out-of-order rows retained together share one aggregate allowance. Raw values, formula text, inline text, and resolved strings count conservatively, including replaced cell records. XML preparation also bounds text read to validate shared-string indices and shared-formula followers before returning a reader. An over-limit read throws InvalidDataException; close that reader before opening a new one with a larger limit for trusted input. Indexed UTF-8 and SDK worksheet-chunk caches retain their existing limits. This character allowance is not a total memory budget: the framework XML parser can buffer an individual CDATA or attribute node before it is charged. It also does not bound objects created by a custom converter.

For eligible plain ASCII shared-string tables, string-cache pages are allocated as values are requested. After disposal, one cleared offset/length index can remain available for later readers, capped at 2 MiB of entry payload plus fixed object overhead. The retained index contains no workbook text or references to package bytes; it trades idle managed memory for lower allocation on repeated opens.

Native XLSB reading also validates the complete shared-string table before returning the reader, including unused entries. Eligible tables retain validated encoded text and create managed strings as values are requested. Borrowed UTF-8 reads can encode that text without first creating a managed string. Larger tables use eager decoding when the encoded-text or entry-index optimization limits are exceeded; the public validation limits and canonical Unicode values are the same.

On .NET 8 and later, TryGetUtf8Text can borrow plain UTF-8 worksheet text, normalized shared-string text, and XLSB string cells:

using OfficeIMO.Data;
using OfficeIMO.Excel;

using var reader = ExcelDocument.OpenDataReader("input.xlsx");
while (reader.Read()) {
    if (reader.TryGetUtf8Text(0, out ReadOnlySpan<byte> text)) {
        ProcessUtf8(text);
    } else if (!reader.IsDBNull(0)) {
        ProcessText(reader.GetString(0));
    }
}

Consume the span before the next Read, ReadAsync, NextResult, NextResultAsync, Close, or Dispose call; copy the bytes if they must live longer. An empty text field can return true with an empty span. false means the value cannot be borrowed, and does not identify a null. Shared strings and XLSB strings use canonical decoded text and a bounded UTF-8 cache; encoding a cache miss can allocate. XLSX inline text requiring XML entity, line-ending, or rich-text decoding, formula cells, schema replay, cell converters, UTF-16 worksheets, and legacy XLS use the ordinary getters. The OfficeIMO.Data.IDataReaderUtf8TextSource capability and IDataRecord.TryGetUtf8Text extension also let generic reader consumers probe for borrowed text without converting unsupported values.

On .NET 8 and later, request DateOnly or TimeOnly explicitly through GetFieldValue<T> or RowsAs<T>. Inferred Excel date/time columns remain DateTime, so moving between target frameworks does not silently change the reader schema. Set ExcelReadOptions.MappingErrorValuePolicy to DataMappingErrorValuePolicy.Redact when typed mapping failures must omit source values and custom-converter exception details; the default is Include for compatibility.

With TreatDatesUsingNumberFormat enabled, calendar formats use the workbook's 1900 or 1904 date system. Time-only and elapsed formats use an OLE Automation DateTime as a carrier for the unshifted serial; changing the workbook date system does not add days to a duration. Use a numeric getter or disable date format interpretation when the application needs the original serial. These rules apply to XLSX, XLS and XLSB tabular readers, including cached numeric formulas. XLS import preserves the source date system and numeric serials rather than reconstructing them from DateTime values. Numeric properties mapped through RowsAs<T> or RowsAsParallel<T> also receive the original serial, including cached formula values. A cell converter returning ExcelCellValue.NotHandled, or a type converter returning (false, null), retains default conversion. Explicit converter results, including handled nulls, take precedence. Type converters see the reader's original value before numeric mapping uses the serial. Explicit RowMapper<T> bindings also support aliases, optional source columns, per-column culture, date formats and converters through RowMappingColumnOptions. These controls apply to sequential, asynchronous and parallel projections.

ExcelReadOptions.EnableWorksheetPrefetch can overlap selected XLSX worksheet decompression with workbook metadata parsing on a spare worker. It is disabled by default, retains the existing package-part limits, and can be slower for small or simple workbooks. Enable it only after measuring a representative workbook on the target CPU.

Choose automatic or ordered parallel reads

Use RowsAsParallel<T>() on the public forward-only reader when typed conversion is substantial enough to repay parallel scheduling. Workbook parsing stays single-owner while independent row snapshots are mapped concurrently and returned in source order:

using OfficeIMO.Data;
using OfficeIMO.Excel;

using ExcelWorkbookDataReader reader = ExcelDocument.OpenDataReader(
    "sales.xlsx",
    new ExcelReadOptions {
        SheetName = "Data",
        A1Range = "A1:D50001",
        InferSchema = true
    });

SalesRow[] rows = reader.RowsAsParallel<SalesRow>(
    new ParallelRowMappingOptions {
        MaxDegreeOfParallelism = 8
    }).ToArray();

For an ExcelSheet, use the explicit ordered-parallel projection API. It supports automatic property mapping, an AOT-friendly RowMapper<T>, or an IDataRecord factory:

using OfficeIMO.Data;

using var document = ExcelDocument.Load("sales.xlsx");
SalesRow[] rows = document["Data"].RowsAsParallel<SalesRow>(
    "A1:D50001",
    new ParallelRowMappingOptions {
        MaxDegreeOfParallelism = 8
    }).ToArray();

Both public surfaces preserve source order and bound in-flight work. The forward-only form applies to every format supported by OpenDataReader; the ExcelSheet form projects an already loaded editable workbook and enables the bounded schema inference required for safe snapshots. Set InferSchema = true on a directly opened reader as shown above. A degree of one, or a schema with provider-owned object/mutable fields, uses the sequential mapping contract. Parsing and decompression are not claimed to run in parallel, and small or cheap rows can still be faster sequentially.

Append to an existing table

Inspect named table definitions with document.GetTables() for the workbook or sheet.GetTables() for one worksheet. The snapshots include names, ranges, columns, filters and table-style metadata. An ordinary cell grid or named range is not a named Excel table.

Worksheet handles remain usable after saving, including when the save rebuilds the underlying package. You can keep an ExcelSheet, save the workbook, then continue editing or inspecting that worksheet. This includes saving a fresh workbook as a template or macro-enabled package and adding VBA to an open workbook. Removed worksheet handles still raise InvalidOperationException when inspected and cannot become handles to replacement sheets. Picture handles and captured mutation plans retain their separate package-lifetime checks.

You can also retain BuiltinDocumentProperties, ApplicationProperties, and individual CustomDocumentProperties values across saves. Later edits update the current workbook; removing or replacing a custom property detaches its previous value. After a transactional mutation rolls back, retained worksheet handles read the restored metadata and can continue changing names and visibility.

using var document = ExcelDocument.Load("sales.xlsx");
var rows = new DataTable();
rows.Columns.Add("Revenue", typeof(decimal));
rows.Columns.Add("Region", typeof(string));
rows.Rows.Add(150m, "APAC");

document["Sales"].AppendDataTableToTable(rows, "SalesTable");
document.Save();

Plan and apply structural edits

using var document = ExcelDocument.Load("report.xlsx");
var sheet = document["Data"];

var plan = sheet.PlanInsertColumns(firstColumn: 2, count: 2);
Console.WriteLine($"{plan.AffectedCells} cells; {plan.Impacts.Count} impact groups");
ExcelMutationResult result = plan.Apply();

sheet.InsertRowsTransactional(firstRow: 5, count: 2);
sheet.Range("A2:C20").CopyTo("E2");
sheet.Range("E2:G20").MoveTo("I2");
sheet.Range("A2:C20").TransposeTo("M2");

document.Save();

Transactional row, column, and cell-shift edits update workbook-owned formulas and names, tables and filters, validations, conditional formatting, merges, links, comments, drawings, charts, sparklines, pivot sources, print definitions, allowed-edit ranges, and ignored-error regions. Copy, move, and transpose use the same bounded dry-run and diagnostic contract. Existing direct row and column methods remain available for callers that have already performed their own gate. Formula results are marked dirty and the workbook requests recalculation on open. Shared formulas are materialized into equivalent normal formulas before the edit so that each member can be rewritten independently.

The operation rejects edits that would cross an array-formula boundary or PivotTable output, remove a table header or totals row, or move a dependent reference beyond Excel's row limit. Remove, move, or resize that owned structure first. Configure scan, affected-cell, rollback-snapshot, and diagnostic budgets through ExcelMutationPlanOptions.

Reference-aware formulas and native cell images

ExcelReference parses and converts A1 and R1C1 cells, ranges, whole rows, and whole columns and provides intersection, union, subtraction, containment, and offset operations. ExcelFormulaSyntaxTree is the shared lossless rewriter used for formulas, defined names, structured table references, chart formulas, pivot sources, print definitions, and structural edits. SearchFormulas searches by text, function, or intersecting parsed reference; formula inspection reports authored, cached, evaluated, dirty, deferred, unsupported, and dynamic-array state explicitly.

Use SetInCellImage, GetInCellImages, and RemoveInCellImage for native rich- value images. Their metadata follows cell sorting, filtering, sizing, copying, moving, and structural edits; they are distinct from floating drawing images.

ExcelImage.HyperlinkUri reads, sets, changes, or removes a native DrawingML picture click link. The target is stored in the workbook; OfficeIMO does not open it or fetch it.

ExcelImage picture = sheet.AddImage(2, 1, File.ReadAllBytes("photo.png"),
    "image/png", widthPixels: 320, heightPixels: 160);
picture.HyperlinkUri = new Uri("https://example.org/photos/1");
picture.HyperlinkUri = null; // Remove the click link.

Image bytes, anchors, and shared drawing relationships are preserved. Read-only workbooks support inspection but reject mutation. Picture handles remain usable after saving while the picture belongs to the workbook. Removing the picture or its worksheet invalidates its handles; a new worksheet with the same name does not restore them. SVG image export preserves targets allowed by the shared safe-link policy and reports unsupported interactive targets. Raster output retains the picture without click behavior.

File-backed editing for large workbooks

const long packageBudget = 2L * 1024 * 1024 * 1024;
using var document = ExcelDocument.OpenFileBacked(
    "large-report.xlsx",
    new ExcelLoadOptions { MaxInputBytes = packageBudget },
    cancellationToken);

document["Data"].CellValue(2, 2, "Updated");
document.Save(new ExcelSaveOptions { MaxTemporaryPackageBytes = packageBudget });

OpenFileBacked stages the editable Open XML package in an owner-only temporary file, copies with fixed memory and deterministic cancellation, and honors load and Open XML part limits. The normal Load, direct writer, streaming reader, and unchanged-package fast paths are unchanged. XLS and XLSB projection continue to use Load.

Validation lists and typed reads

using var document = ExcelDocument.Load("input.xlsx");
var sheet = document["Data"];

sheet.ValidationList("C2:C100", new[] { "New", "Processed", "Hold" });
sheet.Range("D2:D100").Validate.WholeNumberBetween(1, 10, errorMessage: "Use 1 through 10");

List<RowModel> rows = sheet.RowsAs<RowModel>("A1:C100").ToList();

// Omitting the range maps the populated worksheet range.
List<RowModel> populatedRows = sheet.RowsAs<RowModel>().ToList();

// For NativeAOT or explicit column control, use the same mapper shape as CSV.
List<RowModel> mappedRows = sheet.RowsAs<RowModel>(map => map
    .FromColumn<string>("Name", static (row, value) => { row.Name = value; return row; })
    .FromColumn<string>("Status", static (row, value) => { row.Status = value; return row; }))
    .ToList();

// Constructor-bound models use an explicit factory and do not require T : new().
List<ImmutableRow> immutableRows = sheet.RowsAs(factory: row => new ImmutableRow(
    row.GetString(row.GetOrdinal("Name")),
    row.GetString(row.GetOrdinal("Status"))))
    .ToList();

public sealed class RowModel {
    public string Name { get; set; } = "";
    public string Status { get; set; } = "";
}

public sealed record ImmutableRow(string Name, string Status);

Charts and dashboard recipes

Combination-chart snapshots retain secondary value-axis bounds, tick units and number formats independently of the primary axis. Configure them with SetValueAxisScale(..., axisGroup: OfficeChartAxisGroup.Secondary) and SetValueAxisNumberFormat("0%", false, OfficeChartAxisGroup.Secondary); managed chart images use the same independent scale. SetSecondaryValueAxis(new OfficeChartValueAxisLayout(...).WithTitle("Margin")) configures these settings together, including optional majorTickMark and minorTickMark appearance. Snapshots reject malformed secondary numeric settings and secondary logarithmic or reversed scales, display units, and explicit numeric crossing values.

Managed chart images resolve source-linked axis formats from the referenced worksheet cells when those cells share one number format. For example, percentage cells formatted as 0% produce percentage axis labels with no decimal places. An explicitly unlinked axis keeps the format supplied to SetValueAxisNumberFormat. Mixed formats, unresolved references, and sources beyond the format-resolution work bounds report ChartAxisNumberFormatApproximation and use the axis's declared format.

using OfficeIMO.Excel;

using var document = ExcelDocument.Create("dashboard.xlsx");
var sheet = document.AddWorksheet("Summary");

sheet.CellValue(1, 1, "Quarter");
sheet.CellValue(1, 2, "Revenue");
sheet.CellValue(2, 1, "Q1");
sheet.CellValue(2, 2, 10);
sheet.CellValue(3, 1, "Q2");
sheet.CellValue(3, 2, 18);
sheet.CellValue(4, 1, "Q3");
sheet.CellValue(4, 2, 24);
sheet.CellValue(5, 1, "Q4");
sheet.CellValue(5, 2, 30);

sheet.AddTable("A1:B5", hasHeader: true, name: "RevenueTable", style: ExcelTableStyle.TableStyleMedium2);
sheet.ChartFromTable("RevenueTable")
    .RevenueTrend("Revenue trend")
    .Size(640, 320)
    .At(row: 1, column: 5);

sheet.AddHistogramChart(new[] { 8d, 10d, 10d, 12d, 15d, 18d }, row: 18, column: 1, binCount: 3);
sheet.AddParetoChart(
    new[] { "Late", "Damaged", "Missing" },
    new[] { 18d, 7d, 3d },
    row: 18,
    column: 10);

document.Save();

Histogram, Pareto, funnel, and waterfall helpers build compatible XLSX charts from raw values. Native ChartEx authoring is available separately for funnel, waterfall, box-and-whisker, treemap, and sunburst layouts:

var modernData = new ExcelChartData(
    new[] { "Qualified", "Proposal", "Won" },
    new[] { new ExcelChartSeries("Deals", new[] { 42d, 18d, 7d }) });

var modernChart = sheet.AddModernChart(
    modernData,
    row: 18,
    column: 10,
    chartType: ExcelModernChartType.Funnel,
    title: "Pipeline");

modernChart.SetTitle("Current pipeline")
    .SetPlacement(row: 18, column: 10, widthPixels: 640, heightPixels: 360);

ExcelModernChart can inspect imported ChartEx objects and change their name, title, supported layout, and one-cell placement without replacing unrelated markup. UpdateData is available only when the ChartEx formulas resolve to OfficeIMO's owned hidden chart-data sheet; visible imported business data is never claimed as writable chart storage. Other imported charts remain formatting-preserving but data replacement is rejected. Use ExcelFormatCapabilityReport.Current.ToMarkdown() when a workflow must choose between XLSX, XLS, and XLSB targets.

Individual chart point styles

Shared chart authoring accepts OfficeChartSeries.WithPointStyles; existing native charts can be edited with chart.SetDataPointStyle(seriesIndex, pointIndex, style). Solid fills, explicit no-fill, outlines, and seven hatch patterns remain editable in Excel and survive save/reopen. A null style clears the point's fill and outline overrides.

Snapshots, managed image export, and PDF export carry these styles through the shared renderer. Pie and doughnut legend swatches follow their slice appearance. Static area charts retain their series fill and report unsupported per-point appearance. See the shared style example.

Pie and doughnut geometry

Use chart.SetRadialLayout(new OfficeChartRadialLayout(90, 70)) on an existing native two-dimensional pie or doughnut chart. Rotation runs clockwise from the top (0–360 degrees); the doughnut hole is an inner-to-outer diameter percentage (10–90). chart.RadialLayout reads the native settings. Save/reopen, data updates, snapshots, managed chart images and workbook PDF exports and semantic HTML round trips preserve them. For an individual pie or doughnut slice, chart.SetDataPointExplosion(seriesIndex, pointIndex, percent) sets an outward offset from 0 to 400 percent without replacing the chart. Pass zero to explicitly remove that slice's offset, or null to restore an inherited series offset.

Pivot tables and pivot-backed charts

using OfficeIMO.Excel;
using System.Linq;

using var document = ExcelDocument.Create("pivot-report.xlsx");
var sheet = document.AddWorksheet("Sales");

sheet.CellValue(1, 1, "Region");
sheet.CellValue(1, 2, "Product");
sheet.CellValue(1, 3, "Quarter");
sheet.CellValue(1, 4, "Revenue");
sheet.CellValue(2, 1, "EMEA");
sheet.CellValue(2, 2, "Alpha");
sheet.CellValue(2, 3, "Q1");
sheet.CellValue(2, 4, 125000);
sheet.CellValue(3, 1, "EMEA");
sheet.CellValue(3, 2, "Beta");
sheet.CellValue(3, 3, "Q1");
sheet.CellValue(3, 4, 94000);
sheet.CellValue(4, 1, "APAC");
sheet.CellValue(4, 2, "Alpha");
sheet.CellValue(4, 3, "Q2");
sheet.CellValue(4, 4, 141000);
sheet.AddTable("A1:D4", hasHeader: true, name: "SalesTable", style: ExcelTableStyle.TableStyleMedium4);

sheet.Pivot("A1:D4")
    .Rows("Region")
    .Columns("Quarter")
    .Filters("Product")
    .Sum("Revenue", "Total revenue", "#,##0")
    .Layout(ExcelPivotLayout.Tabular)
    .Style("PivotStyleMedium9")
    .Captions(rowHeader: "Region", columnHeader: "Quarter", grandTotal: "Total")
    .At("F2", "SalesPivot");

sheet.CellValue(5, 1, "APAC");
sheet.CellValue(5, 2, "Beta");
sheet.CellValue(5, 3, "Q2");
sheet.CellValue(5, 4, 87000);
sheet.UpdatePivotTableSource("SalesPivot", sheet, "A1:D5");
document.AddPivotSlicer(
    "SalesPivot",
    "Region",
    sheet.Name,
    new ExcelSlicerViewOptions { Name = "RegionFilter", Row = 12, Column = 8 });

var pivot = sheet.GetPivotTables().Single(p => p.Name == "SalesPivot");
Console.WriteLine($"{pivot.Name}: {string.Join(", ", pivot.RowFields)}");

var chart = sheet.ChartFromTable("SalesTable")
    .VarianceColumns("Revenue by region")
    .At(row: 12, column: 1);
chart.SetPivotSource("SalesPivot");

document.Save();

Pivot support covers source-range pivots, row/column/page/data fields, styles, layouts, filters, calculated fields, grouping metadata, shared-cache-aware source updates, refresh-on-open, and readback. AddPivotSlicer authors native slicer caches, worksheet views, and drawing anchors for supported fields. AddPivotTimeline does the same for date-only fields. Compatible views reuse shared caches; removing the last view can prune its cache. Unsupported imported siblings remain preserved.

Read an existing saved pivot value with GetPivotData. The lookup uses the pivot's saved output cells and axis metadata; changing source cells does not refresh it.

using var document = ExcelDocument.Load("saved-sales-pivot.xlsx");
var sheet = document.GetSheet("Summary");
ExcelCellData value = sheet.GetPivotData("SalesPivot", "Revenue",
    new Dictionary<string, object?> { ["Region"] = "North" });

The lightweight evaluator also supports GETPIVOTDATA("Revenue",Summary!A1,"Region","North"). Both paths support saved hierarchies with multiple row and column fields, selected page items, multiple measures, default subtotals, and text, numeric, Boolean, date, error or blank item keys. Numeric range groups and Years/Months or Years/Quarters/Months date hierarchies on row or column axes use their displayed group labels as criteria. A numeric criterion equal to a group's lower bound also selects that group; other raw source numbers do not. A year criterion accepts its displayed text or an integer year. Lookup follows saved tabular, compact and outline views, including subtotals displayed at the top and collapsed groups. A partial selection uses its displayed subtotal, or a single matching leaf when no subtotal is displayed. Ambiguous matches and unknown items, fields or measures return typed errors; a blank intersection of existing items returns zero. Lookup accepts at most 256 source fields, measures and criteria, one Values pseudo-field across the axes, 100,000 shared keys per field, 100,001 axis/field items (including totals or default items), and one million output cells. Each axis also has a one-million budget for field visits and indexed criterion items. Other grouped profiles and views without saved axis items remain unsupported. The public method throws NotSupportedException for those profiles; formula recalculation leaves them unsupported. AddPivotTable authors metadata and refresh-on-open settings; it does not populate an output view for lookup.

For date items, use a DateTime or a numeric serial in the workbook's date system. Use serial 60 (or its time fraction) to select Excel's fictitious February 29, 1900; a .NET DateTime cannot represent that date. Pivot-cache date items retain Excel's separate OLE Automation encoding, including early-1900 values. When a mixed field contains both a date and a number with the same serial, lookup selects the date item, as Excel does. To select an error item through the public API, pass new ExcelCellData(ExcelCellDataKind.Error, "#DIV/0!") as the criterion value. The string "#DIV/0!" selects a text item with that spelling. An error-valued formula argument propagates its error through formula calculation.

Call MaterializePivotTable to generate an output view without an Office refresh:

ExcelPivotMaterializationResult result = sheet.MaterializePivotTable("SalesPivot");
Console.WriteLine(result.OutputRange);

For a typed OrderDate column, build and populate a year/month view in the same workbook:

sheet.Pivot("A1:B6").Rows("OrderDate").Sum("Sales", "Metric")
    .DateHierarchy("OrderDate", ExcelPivotGroupBy.Years, ExcelPivotGroupBy.Months)
    .Layout(ExcelPivotLayout.Tabular).At("D4", "SalesByMonth");
sheet.MaterializePivotTable("SalesByMonth");

For a text Product column, create a named manual group in a derived field and materialize the resulting two-level row view:

sheet.Pivot("A1:B6").Rows("Product").Sum("Sales", "Metric")
    .Layout(ExcelPivotLayout.Tabular).At("E4", "SalesByProductGroup");
sheet.AddPivotManualGrouping("SalesByProductGroup", "Product", "Product2",
    new Dictionary<string, string[]> { ["Fruit"] = new[] { "Apple", "Pear" } },
    hiddenGroupItems: new[] { "Carrot" });
sheet.MaterializePivotTable("SalesByProductGroup");
var fruit = sheet.GetPivotData("SalesByProductGroup", "Metric",
    new Dictionary<string, object?> { ["Product2"] = "Fruit" });

Pass multiple entries in the dictionary to add several named groups in one derived field. Each source item can belong to only one group; ungrouped text items remain individual entries. The source field must be on a row or column axis. Use the pivot's actual name, which may differ from a requested duplicate name.

Materialization supports up to 256 ordinary measures with unique captions, all eleven aggregation modes, and multiple fields on each axis, with at most 256 cache fields in total. Numeric range groups with explicit finite bounds and intervals, including decimal intervals, are supported when each bound and interval is at most 9 × 10¹⁵ in magnitude and the range has at most 99,998 buckets. Years/Months and Years/Quarters/Months date hierarchies and manual text groups with saved group labels are also supported on row and column axes. Hidden items on qualified manual, numeric, and date groups are applied to source records and saved views. Grouping on page fields is not. Date hierarchies require typed date source values. The saved group labels are retained, including labels created under a non-English Excel locale. Renamed item captions remain visible in the generated view; GETPIVOTDATA accepts both the displayed caption and the original cache key for a renamed ordinary item. It writes a tabular view in first-seen key order for ordinary fields, chronological order for default-sorted date fields with native date filters, and group-label order for groups, with selected page items, hidden row/column items, optional grand totals, typed values/errors, source cache records and consistent axis metadata. It saves a copy of the source records in the pivot cache and clears refresh-on-open. Source formulas must have saved cached results; materialization does not calculate them. When several pivot views share a cache, materializing any one of them regenerates every view of that cache in one transaction. All views must qualify for materialization and produce identical cache fields and records. The returned AffectedPivotTables names the refreshed views; OutputRange remains the range of the requested view. Manual item filters retain their selected keys when source item order changes. New source keys remain excluded from a manually filtered field unless its IncludeNewItemsInFilter setting admits them. A filter that leaves no source records is rejected before changing the saved view. Materialization supports label equals, begins with, ends with, contains, and their negations on ordinary row or column fields with text, Boolean, blank, error, or numeric captions. Numeric captions use General, #,##0, #,##0.00, #,##0.000, 0.0, 0.00, 0.000, 0.0%, 0.00%, $#,##0, $#,##0.00, or #,##0.00;(#,##0.00) pivot-field formatting; other numeric formats and date captions remain unqualified. It also supports label greater/less comparisons and inclusive between or exclusive not-between ranges when every caption and criterion uses ASCII letters. Numeric label ranges support signed integer captions in General, signed fixed two-decimal captions in 0.00, and nonnegative grouped integer captions in #,##0. These use caption order rather than numeric magnitude; in the qualified Excel workbooks, 2 precedes -2. Other text and numeric formats are rejected because Excel's localized ordering or display format can select different items. Qualified label comparisons use the current process culture, matching Excel's locale-dependent ordering when run under the same locale. Distinct numeric cache keys remain separate when the format gives them the same displayed caption: a label filter can select both rows, while typed numeric GetPivotData criteria still address each key.

Native fixed-date equals/not-equals, older/newer, inclusive-bound, between, and not-between filters materialize on date-valued row or column fields. By default they compare complete date-time values in either workbook date system, so a midnight equality does not select a later time on the same day. Call WithWholeDay(), for example ExcelPivotFilter.DateEquals("OrderDate", day).WithWholeDay(), to include every time on the selected date. The same modifier applies to the other fixed-date predicates; it is saved in Excel's pivot-filter extension and survives a workbook reload. GetPivotTables() exposes the setting through each filter's WholeDay property. All-years month and quarter selectors include matching dates from every year, including time-bearing values. Relative today/yesterday/tomorrow, week, month, quarter, year, and year-to-date filters use local calendar periods with half-open end bounds; weeks begin on Sunday, as in the qualified Excel-produced workbooks. The local date is captured once for every view sharing a cache. Pass referenceDate: new DateTime(2026, 9, 28) to MaterializePivotTable for a reproducible relative-date view; omitting it uses the current local date. Default-sorted date items selected by these filters are ordered chronologically in the saved cache and view. Views sharing that cache retain their own saved manual item order. Blank source dates do not match, including not-equals.

On a single ordinary axis field, materialization also supports value equals, not equals, greater than, greater than or equal, less than, less than or equal, inclusive between, exclusive not-between, and top/bottom count, percent, and sum filters. Count filters include ties at the cutoff. Percent and sum filters select by cumulative signed measure value, including the item that crosses the threshold. Percent uses the current grand total; a zero grand total retains all items. When a percent or sum cutoff splits equal aggregates, materialization reports an unsupported item order rather than writing a different filtered view. Zero and negative item aggregates are supported. An item whose selected aggregate is an error does not enter a ranking with numeric aggregates; Count and Count Numbers still rank their numeric results for error-valued source cells. When every selected aggregate is one of Excel's seven classic errors, top/bottom count filters rank by Excel error-code order and include ties at the cutoff. All-error percent and sum ranking, newer error values, and ranking with neither numeric nor qualified error aggregates remain deferred. With two ordinary row fields, no column field, and one measure, materialization supports the same value comparisons and ranges on either field and top/bottom count, percent, and sum on either field. The inner field's aggregate is compared or ranked within each parent; the outer field uses its aggregate across its children. Two column fields support the same comparisons, ranges, and top/bottom count, percent, and sum filters on either field. With one row and one column field, the same value comparisons, ranges, and top/bottom count, percent, and sum filters can target either field. With three row fields and one measure, Top 1, Top 2, and Bottom 1 count, numeric greater-than, and numeric between filters on the innermost field are qualified against Excel-produced views and GETPIVOTDATA results. With two row fields, one column field, and one measure, numeric greater-than, numeric between, and Top/Bottom 1 count on the inner row field, plus numeric greater-than and Top/Bottom 1 count on the column field, are qualified against complete Excel-produced views and lookups. Top 1 includes tied column items at the cutoff, and the qualified inner-row Top 1 case retains tied siblings. Numeric Top 1 ranking also excludes error-valued aggregate siblings on either qualified axis. A saved inner-row Top 1 followed by column Bottom 1 is evaluated in that order; the reverse filter order can select different cells, as in Excel. Other three-or-more-field value rules remain outside this qualified profile. With two row fields, one column field, and two Sum measures, an inner-row Top 1 filter can select either named measure. Excel-produced views and lookups qualify both selections with Values before or after Channel on columns, or before Region, between Region and Product, or after Product on rows. Use ValuesPosition(0) or ValuesPosition(1) on the fluent pivot builder to place Values first or between the two row fields. When Values precedes Product on rows, the Top 1 filter selects items only for its named measure; the other measure keeps its full source rows. Other multi-measure filter interactions remain outside this profile. Signed and zero child aggregates, nonpositive outer-field totals, three siblings per parent, mixed numeric/error ranking, and classic all-error parent top/bottom count ranking have Excel-produced examples. All-error parent percent/sum ranking and broader sign/error mixes still need independent evidence. Label and value filters can share that field. Value filters use the selected measure's aggregation over current source rows; refreshed views reevaluate item membership. The filter definition remains available for Excel refresh, and filtered items are not converted into manually hidden items. Filter evaluation is bounded by the source-cell work budget. For multiple measures, the Values field appears on exactly one axis. Values-first, intermediate and Values-last ordering are preserved, including views without real row or column fields. AddPivotTable creates this field on columns by default, or on rows when dataOnRows is true. Repeated native axis prefixes are expanded during saved-view lookup. Regeneration expands collapsed groups and normalizes compact and outline layouts to tabular output with subtotals below their children. Hidden subtotals stay hidden; custom subtotal functions become automatic subtotals using each measure's aggregation. Date labels receive date/time formatting when the destination has no date format. Date subtotal captions use yyyy-MM-dd HH:mm:ss, including Excel's fictitious February 29, 1900. Existing destination date formats are retained.

The operation rejects destination collisions with unrelated values, formulas, source cells, other pivots, tables and merged ranges. It replaces a previous materialized view and clears its stale tail while retaining existing cell styles. Preparation and writing run under the workbook lock; the shared mutation owner rolls back interrupted writes or invalid generated metadata. Use ExcelMutationPlanOptions to lower source/output and rollback budgets. The source and affected output are each capped at one million cells. The measure-input budget is source records times measure count times the number of displayed input levels on each axis: the leaf level plus enabled intermediate subtotals. Grand totals add at most one level per axis, bounding actual aggregate updates at four times that budget. For shared caches, source-cell visits, measure-input visits, and affected output cells are each summed across views against the configured budget. Fields and measures are capped at 256; distinct shared keys per field and observed leaf aggregate states across measures are capped at 100,000. All aggregate states, including subtotals and grand totals, must fit the output budget. Saved axis items, including totals, are capped at 100,001. On each axis, the sum of saved field items and shared keys must also fit the configured budget, so all generated criteria combinations remain within the saved-lookup indexing limit.

Other date levels and layouts, custom non-text grouping, formatted date and broader numeric label filters, localized label range ordering, explicit show-all empty item combinations, other label/value pivot filter types and broader multi-field value filtering, calculated fields, incompatible shared-cache groupings, and associated interaction caches still require additional materialization support. Imported definitions remain available through the existing metadata and refresh-on-open APIs.

Guarded query-backed tables

using var document = ExcelDocument.Create("query-report.xlsx");
var sheet = document.AddWorksheet("Results");

var query = document.AddQueryBackedTable(new ExcelQueryBackedTableOptions {
    ConnectionName = "SalesQuery",
    CommandText = "sales/current",
    WorksheetName = sheet.Name,
    StartCell = "B3",
    TableName = "SalesResults",
    ColumnNames = new[] { "Region", "Amount" }
});

ExcelQueryRefreshResult refresh = await document.RefreshQueryAsync(
    query.ConnectionName,
    applicationQueryHost,
    new ExcelQueryExecutionPolicy {
        AllowExecution = true,
        MaximumRows = 100_000,
        MaximumCells = 500_000
    },
    cancellationToken);

OfficeIMO stores the native connection, table, and query-table relationship chain but does not ship a database or network provider. The application-owned IExcelQueryExecutionHost interprets the opaque command and returns rows. OfficeIMO applies row, column, cell, and character budgets before a transactional table replacement. Commands loaded from imported workbooks require the separate AllowImportedCommands opt-in.

Formula inspection and calculation policy

RATE, IRR, XIRR and XNPV calculate annuity rates and periodic or dated cash flows. Cash-flow vectors accept worksheet ranges, names, simple structured references and rectangular literal arrays. IRR ignores text, Booleans and blanks in its series; XIRR retains blank cash-flow positions as zero. Dated functions truncate date serials, use a 365-day year, and require dates no earlier than the first date. XNPV requires a positive discount rate. RATE/IRR use at most 20 iterations per attempt and XIRR at most 100. An unsuccessful nondefault guess gets one retry from 10%; non-convergence returns #NUM!. Guesses select among possible roots, so a cash-flow schedule does not imply a unique rate. The shared 100,000-cell resolution budget applies across paired vectors. Saved Microsoft Excel references cover successful rates, multiple roots, negative returns, date fractions, input coercions and error results.

IPMT and PPMT calculate the interest and principal portions of a constant-rate annuity payment, including beginning-of-period payments and omitted optional future-value/payment-type arguments. For example, IPMT(0.1/12,1,36,8000) calculates the first month's interest. These scalar functions use the same PMT/FV owner; typed errors propagate through references and IFERROR. The qualified rate domain is greater than -1, zero payment type means end-of-period and nonzero means beginning-of-period, and the selected period is between 1 and the positive period count. Invalid qualified domains produce #NUM!; unsupported signatures and operands retain their existing cached result rather than inventing a value.

Scalar calculation supports parentheses, unary signs, percentages, powers, arithmetic precedence, text concatenation, comparisons, and expressions inside supported function arguments. For example, SUM(A1,2)*3 and IF(A1*2>10,"High","Low") can be calculated without Excel. Errors such as #DIV/0!, #VALUE!, and #NUM! propagate through these expressions. Syntax and nested function evaluation are bounded to 128 levels; unsupported formulas continue to be reported explicitly rather than calculated from stale caches.

INT, MOD and SQRT evaluate finite numeric scalar arguments, including references and nested expressions. Boolean operands retain the existing 0/1 coercion. INT floors negative values; MOD uses the divisor's sign. A negative square root produces #NUM!, a zero divisor produces #DIV/0!, and a nonfinite result produces #NUM!. These errors replace old numeric caches and propagate through arithmetic and error handlers. Invalid argument counts, multi-cell scalar ranges and unsupported operand types remain unevaluated.

EXP, LN, LOG and LOG10 evaluate finite numeric scalar operands with strict argument counts. LOG defaults to base 10 and also accepts an explicit positive base. Nonpositive logarithm arguments or bases produce #NUM!; base 1 produces #DIV/0!, and exponential overflow produces #NUM!. Typed errors propagate through references, arithmetic and IFERROR. Unsupported operands and signatures remain unevaluated and preserve existing caches. Results use floating-point math, so inverse functions need not return exact original values.

SIGN, TRUNC, ROUNDUP and ROUNDDOWN also use scalar arguments with exact supported signatures. TRUNC defaults omitted digits to zero. Digit arguments are supported from −15 through 15; other values remain unevaluated. Directional rounding uses the same decimal normalization as ROUND, so exact decimal boundaries such as TRUNC(0.29,2) and ROUNDUP(0.07,2) stay unchanged. Large integral values avoid unnecessary scaling, and nonfinite results produce #NUM! instead of invalid numeric caches. All operands, including supported digit bounds, qualify before an argument error is propagated.

COMBIN evaluates two finite scalar operands with a nonnegative item count up to 2^53−1, truncating fractions. Negative inputs, a chosen count larger than the item count, and results beyond the numeric range return #NUM!. Integer arithmetic avoids factorial overflow and accumulated rounding; large finite results use the workbook’s floating-point representation. Evaluation takes at most 1,023 steps. Unsupported signatures, ranges, text and larger item counts retain existing caches.

GCD and LCM accept one to 255 arguments: finite numeric values, invariant numeric text, and cell or rectangular range references sharing a 100,000-cell budget. Fractions truncate toward zero. Blank direct cell arguments are ignored; if every direct argument is blank, the result is #VALUE!. Blank positions inside a range contribute zero. Negative operands produce #NUM!, while Boolean and nonnumeric text produce #VALUE!. Typed errors propagate. Operands outside ±(2^53−1), unsupported expressions and resource-limit failures retain existing caches, including inside error handlers. LCM returns #NUM! when its nonzero result exceeds 2^53−1; a zero anywhere in the arguments returns zero even if an intermediate product would exceed that bound. Native Numbers and fresh Excel results qualify the recorded cases; Numbers reconstruction has its own narrower contract. The blank-reference qualification also records native fill/save/clear results and saved error caches for all-blank direct arguments.

FACT evaluates one finite scalar, truncating nonnegative fractions before multiplication. Zero returns one. Negative inputs and values at least 171 produce #NUM!; at most 169 multiplications are needed for a finite result. Large factorials use floating-point arithmetic. Typed errors propagate, while unsupported operands and signatures preserve existing caches.

EVEN and ODD round one finite scalar away from zero to the nearest integer of the requested parity. EVEN(0) is zero and ODD(0) is one. Typed errors propagate; unsupported operands and argument counts retain existing caches. ODD retains its cache when the result would exceed ±(2^53−1), because larger odd integers cannot be represented exactly by the evaluator's numeric type. EVEN supports larger finite values that are already even binary64 integers.

CEILING and FLOOR require two scalar operands and treat explicit omitted operands as zero. Negative numbers with negative significance round away from zero for CEILING and toward zero for FLOOR; positive significance rounds negative numbers toward positive and negative infinity, respectively. Positive numbers with negative significance produce #NUM!. Zero numbers return zero; zero significance returns zero for CEILING and #DIV/0! for nonzero FLOOR. Decimal normalization preserves exact multiples such as CEILING(0.07,0.01). Remainder-based calculation avoids quotient overflow for finite rounded results; nonfinite results produce #NUM!. Typed errors propagate, while unsupported operands, ranges and argument counts preserve existing caches. The .MATH variants retain their separate direction and mode contracts.

OFFSET resolves a single-cell value or a rectangular reference argument for functions such as SUM, within worksheet bounds and 32 reference levels. Multi-cell scalar results, implicit intersection and spills remain unsupported. PROB evaluates finite numeric vectors with a shared 100,000-cell input budget; mixed text, Boolean and blank vector coercion remains unsupported. Probabilities must lie between zero and one and sum to one within 1e-12 rounding tolerance. RANDBETWEEN accepts finite bounds from -9007199254740991 to 9007199254740991 (the consecutive exact integer range through ±(2^53−1)). It rounds the lower bound up and the upper bound down, then draws a fresh integer from that inclusive interval on each calculation pass; dependent formulas reuse that pass's cell cache. Reversed bounds produce #NUM!. Intervals containing no integer and bounds outside that range retain their producer cache without local evaluation. Inspection reports direct references; dynamic OFFSET targets are resolved during calculation and its runtime dependency guards apply. Invalid reference errors propagate through reference consumers and error handlers. Unsupported inputs and resource limits remain unevaluated inside IFERROR, IFNA and error predicates, preserving existing caches. Completed supported lookup searches return typed #N/A when no match exists.

XLOOKUP retains empty optional argument positions: an empty fallback uses #N/A, an empty match mode uses exact matching, and an empty search mode scans from the first item. The fresh Excel reference records no-match, explicit-fallback and reverse-search cases. Conditional aggregates resolve unquoted criteria as formula arguments; unsupported criterion expressions preserve existing caches. Quoted criteria remain literal text.

Named references can resolve to A1 ranges or numeric, text, Boolean, and error constants, including bounded aliases and worksheet-local scope. Arbitrary formulas stored in defined names remain outside this calculation subset. ISNUMBER distinguishes numbers from Boolean values; ISLOGICAL and NA support typed guards and error fallbacks. SEARCH and text criteria recognize *, ?, and tilde escapes. ROUND handles decimal midpoints away from zero. TEXT uses invariant English formatting and accepts an explicit [$-409] locale prefix; other locale qualifiers remain unsupported.

Array calculation uses the range owned by SetArrayFormula, or the existing array reference in an imported workbook. A fixed range must match the result shape:

using var document = ExcelDocument.Create("array-report.xlsx");
var sheet = document.AddWorksheet("Results");
sheet.SetArrayFormula("G2:H4", "SEQUENCE(3,2,10,-2)");
document.Calculate();
document.Save();

Use SetDynamicArrayFormula when the result shape may change:

using var document = ExcelDocument.Create("dynamic-report.xlsx");
var sheet = document.AddWorksheet("Results");
sheet.CellValue(1, 1, 3);
sheet.SetDynamicArrayFormula("G2", "SEQUENCE(A1,2)");
document.Calculate(); // G2:H4 contains 1 through 6
document.Save();

SEQUENCE produces rectangular numeric results. FILTER accepts a row or column mask of numbers or Booleans, including comparisons between operands of the same native type and a scalar empty-result fallback. Blank mask cells are false; mixed-type comparisons and text-mask coercion remain deferred. SORT supports numeric, ASCII letter-and-digit text, Boolean, and blank sort keys, including mixed keys, both directions, and sorting rows or columns. Numbers precede text and Booleans in ascending order; descending order reverses those nonblank groups. Blanks remain last in either direction and their output cells contain zero, as in Excel's cached results. Text compares without ASCII case and equal keys keep their input order. Sort inputs dependent on an unevaluated formula cache remain deferred. Locale-dependent, punctuation, Unicode, and error-key collation remains deferred. UNIQUE preserves the first occurrence of distinct rows or columns and supports exactly_once. These functions can be nested within this array subset. Each input and output is limited to 100,000 cells, with at most 32 array-expression levels. SetArrayFormula writes the Excel compatibility prefixes for these four functions, including nested calls, while preserving literals and reference text.

Fixed arrays update only when their result matches the authored range and the range contains neither another formula nor a merged cell. Dynamic arrays write Excel's native dynamic-array metadata, resize their cached range as inputs change, and clear vacated children. A nonempty cell, fixed array, table, or merged range that blocks a spill yields #SPILL! at the anchor without overwriting the blocker. Clearing the blocker allows the next calculation to spill. Value, formula, bulk-value, and clear operations reject edits inside an active spill; call ClearArrayFormula to remove the array first. Array children participate in same-sheet dependency calculation before caches are written; a reference to the anchor is scalar. Unsupported expressions remain deferred with existing caches preserved. Empty results use Excel's rich-value #CALC! metadata. Zero-sized SEQUENCE dimensions produce #CALC!; negative dimensions produce #VALUE!. The checked-in Excel-produced array corpus verifies cached results through save and reopen.

Loaded dynamic arrays can resize when their saved child caches still match the worksheet's original content. If old cells were edited outside the sheet API, the saved source changed before calculation, the raw Open XML package was accessed before the sheet, or the source exceeds the bounded ownership scan, calculation reports #SPILL! and keeps ambiguous cells. Clear and re-author the array only when you own the old range.

Cached reads resolve native rich-value #CALC! and #SPILL! errors across the object model, range reads, and forward-only data readers. Unknown or unresolved rich values retain their ordinary cell fallback. Rich error metadata is limited to 16 MiB per part and 100,000 entries per collection; readers also honor a smaller configured metadata limit. Repeated calculation reuses error records, and images and other rich values keep their existing metadata and relationships.

CellAt(...).GetValue().Value preserves the native cached result type, including Boolean values and text that looks numeric, such as the result of ="12".

Calculation honors the workbook's 1900 or 1904 date system. In the 1900 system, DATE and date-part functions retain Excel's fictitious February 29, 1900. When converting serial 60 to DateTime, ExcelDateSystemConverter.FromSerial uses February 28 because .NET cannot represent that fictitious date. The checked-in Excel-produced corpus verifies function composition, named and cross-sheet references, typed errors, rounding, and both date systems through calculation, save, and reopen.

Calculation.MaximumDependencyDepth defaults to 256 formula cells per chain. The expression-nesting limit applies separately within each cell. A dependency limit or circular reference clears affected caches and marks those formulas dirty; unrelated formulas still calculate. Raise the dependency budget explicitly for longer trusted chains. Calculation also checks the caller's available stack; deeply composed dependencies can be left dirty even within those two budgets. The independent corpus includes forward and reverse 300-cell chains and a circular-reference workbook, with cache recovery and diagnostics verified after save and reopen. Circular caches from Excel are preserved as input evidence rather than treated as a numeric cycle solution.

For bounded row-memory XLSX exports, set RequireStreaming = true in ExcelTabularWriteOptions. With WriteDataReader, also set UseSharedStrings = false and AutoFit = false; tables require headers. With WriteRows, turn off tables and automatic sizing. Incompatible options are rejected before source rows are consumed or destination bytes are written.

using var document = ExcelDocument.Load("report.xlsx");

var formulas = document.InspectFormulas();
Console.WriteLine(formulas.ToMarkdown());
Console.WriteLine($"Maximum dependency depth: {formulas.DependencyGraph.MaximumDependencyDepth}");

foreach (var cycle in formulas.DependencyGraph.CircularReferences) {
    Console.WriteLine("Circular: " + string.Join(" -> ", cycle.References));
}

foreach (var formula in formulas.Formulas.Where(f => !f.IsSupportedByOfficeIMO)) {
    Console.WriteLine($"{formula.SheetName}!{formula.CellReference}: {formula.UnsupportedReason}");
}

document.Calculation.MaximumDependencyDepth = 512;
int calculated = document.Calculate();
document.Save("report.xlsx", new ExcelSaveOptions {
    EvaluateFormulasBeforeSave = true,
    ForceFullCalculationOnOpen = true
});

TEXTJOIN formula storage

Author TEXTJOIN through the normal cell and array-formula APIs. The writer stores these calls as _xlfn.TEXTJOIN in XLSX, including nested calls; GetFormulaText exposes the stored spelling. Existing prefixes, string literals, quoted worksheet names and structured references retain their text. Prefix expansion is checked against the 8,192-character formula limit.

Calculate() supports both empty-cell policies, quoted worksheet names with punctuation, and typed error propagation through TEXTJOIN, CONCAT and CONCATENATE. Ordinary text such as #N/A remains text. Joined results above the 32,767-character cell-text limit become #VALUE!. The evaluator remains bounded by its supported expression and range contracts. Rejected prefix expansion preserves existing cells and in-cell images.

IFNA, MINIFS and MAXIFS formula storage

Author these functions through CellFormula or SetArrayFormula using ordinary names. The shared writer stores their XLSX future-function prefixes, including nested calls: IFNA(NA(),99) becomes _xlfn.IFNA(NA(),99). GetFormulaText returns the stored spelling. String literals, quoted sheet names and existing prefixes retain their text; oversized prefix expansion rejects the write before changing cells.

The native Excel reference covers 32 normal and single-cell array formulas, typed errors, no-match extrema and a native operand edit. Buffered and materialized authoring, saved/reopened calculation and escaped literal/reference text have separate regression coverage. Broader future functions retain their own qualification gates.

Referenced values in aggregates

AVERAGEIF and AVERAGEIFS return typed #DIV/0! when fully qualified inputs yield no numeric matches. IFERROR can handle that error; IFNA propagates it. Unsupported expressions, unresolved inputs and range-budget failures remain unevaluated and retain their existing caches. Selected typed errors propagate through conditional sums, averages and extrema. Typed error criteria match the same error type and remain distinct from ordinary text such as "#N/A". The nonblank criterion "<>" is supported. No-match MINIFS/MAXIFS return zero; extrema over matched nonnumeric values remain unevaluated. The fresh native Excel reference covers empty averages, 98 typed-error and criterion cases, dependent error handlers, save/reopen and operand edits.

Calculate() preserves referenced types for numeric aggregates. MINA, MAXA and AVERAGEA include Boolean values as one or zero and referenced text as zero, skip blank cells, and preserve typed errors. Empty MINA and MAXA ranges return zero; empty AVERAGEA ranges return #DIV/0!. Ordinary numeric aggregates skip referenced Boolean and text values, including numeric-looking strings. Boolean formula results and selected text results retain their types through references and saved caches. SUMSQ, LARGE and SMALL filter referenced data values; the rank argument keeps scalar coercion. Scalar numeric coercion remains available. Positional statistical helpers require numeric-only ranges; mixed-type series remain unevaluated rather than losing their paired coordinates.

Preflight a workbook before choosing a workflow

using var document = ExcelDocument.Load(
    "incoming.xlsx",
    new ExcelLoadOptions { AccessMode = OfficeIMO.DocumentAccessMode.ReadOnly });

ExcelFeatureReport report = document.InspectFeatures();

try {
    report.EnsureCan(ExcelPreflightCapability.EditWorkbookStructure);
} catch (InvalidOperationException ex) {
    Console.WriteLine(ex.Message);
}

if (!report.Can(ExcelPreflightCapability.ExportPdfReport)) {
    Console.WriteLine(report.ToMarkdown());
}

Use workflow preflight when an application needs to decide whether a workbook is safe for readback, cell-value edits, structure-changing edits, cached-formula reads, OfficeIMO formula calculation, template binding, or first-party PDF report export. Preserve-only features such as macros, unsupported imported interaction markup, threaded comments, external links, custom XML, OLE objects, and form controls are reported with package details instead of being silently ignored.

Package and VBA signatures

InspectSignatures() and InspectPackageSignatures(...) remain provider-free. Pass an optional security provider only when creating a package signature or validating its cryptography, package digests, certificate chain, and revocation. OPC timestamp elements are reported as structural evidence; this shared result surface does not claim timestamp-token validation:

using OfficeIMO.Security;

IOfficeSecurityProvider security = OfficeSecurityProvider.Default;
ExcelDocument.SignPackageSignature("report.xlsx", security, signingCertificate);
OfficePackageSignatureValidationReport validation =
    ExcelDocument.ValidatePackageSignatures("report.xlsx", security);

InspectVbaSignatures(...), ValidateVbaSignatures(...), and SignVbaProject(...) use the managed bounded VBA core shared with Word and PowerPoint. It creates and validates legacy, agile, and V3 carriers in .xlsm, .xltm, .xlam, and .xlsb on every supported platform through an explicit IOfficeSecurityProvider. A registered Microsoft Office SIP is an optional Windows differential check, not a signing dependency. Sign the VBA project before applying an OPC package signature.

DataTable and JSON exchange

using System.Data;

using var document = ExcelDocument.Load("data.xlsx");
var sheet = document["Data"];

DataTable table = sheet.ToDataTable("A1:C100");
string json = sheet.ToJson("A1:C100");

sheet.FromJson("[{\"Name\":\"Gamma\",\"Amount\":30}]", startRow: 8, startColumn: 1);

Template markers

using var document = ExcelDocument.Load("invoice-template.xlsx");

int replacements = document.ApplyTemplate(new Dictionary<string, object?> {
    ["Invoice.Number"] = "INV-001",
    ["Customer.Name"] = "Adatum",
    ["Total"] = 123.45m
});

var template = document.InspectTemplate(new {
    Invoice = new { Number = "INV-001" },
    Customer = new { Name = "Adatum" },
    Total = 123.45m
});

template.EnsureAllMarkersBound();
document.Save("invoice.xlsx");

Comments and conditional formatting

using var document = ExcelDocument.Load("review.xlsx");
var sheet = document["Data"];

sheet.SetComment("A1", "Review total", author: "Alice", initials: "AA");
sheet.UpdateComments(new ExcelCommentFilter { TextContains = "total" }, "Total reviewed", author: "Carol", initials: "CC");

sheet.AddConditionalColorScale("C2:C100", "#FFF0F0", "#70AD47");
sheet.Range("D2:D100").ConditionalFormat.DataBar("#5B9BD5");

// The same lifecycle covers imported classic and Office extension rules.
var rules = sheet.GetConditionalFormattingRules("C2:D100");
var copied = sheet.CloneConditionalFormattingRule(rules[0], "E2:E100");
copied.Priority = 1;
sheet.UpdateConditionalFormattingRule(copied);

document.Save();

For extension-only visuals, use the same format-neutral model rather than a second Open XML-specific API:

sheet.AddConditionalFormattingRule(new ExcelConditionalFormattingInfo {
    Source = ExcelConditionalFormattingSource.Office2010Extension,
    Range = "F2:F100",
    Type = "DataBar",
    DataBarColor = "FF4472C4",
    DataBarBorderColor = "FF203864",
    DataBarNegativeColor = "FFC00000",
    DataBarAxisColor = "FF000000",
    DataBarBorder = true,
    DataBarGradient = false,
    DataBarThresholds = new[] {
        new ExcelConditionalFormatThreshold { Type = "AutoMin" },
        new ExcelConditionalFormatThreshold { Type = "AutoMax" }
    }
});

GetConditionalFormattingRules, AddConditionalFormattingRule, UpdateConditionalFormattingRule, CloneConditionalFormattingRule, ReorderConditionalFormattingRules, RemoveConditionalFormattingRule, and ClearConditionalFormatting manage standard and Office extension rules through one API. Common edits do not reserialize unchanged formulas or visuals, so unrecognized imported attributes and extension children are retained. Excel image/PDF projection emits stable diagnostics when extension semantics are approximated or omitted; native XLS export rejects extension-only rules rather than silently discarding them.

Set several column widths

Use SetColumnWidths to apply positive widths in one worksheet update:

sheet.SetColumnWidths(new Dictionary<int, double> { [1] = 24, [2] = 14, [3] = 18 });

Indexes are 1-based. The method preserves column styles, visibility and outline metadata. It validates the complete map before changing widths and clamps widths above Excel's 255-character limit. Use SetColumnWidth to clear an individual custom width with a non-positive value. Manually assigned widths do not mark a column as already auto-fitted. A later AutoFitColumns or AutoFitColumnsFor can resize them, including after reopening the workbook.

For sparse cells, CellWrapTextFor applies wrapping together and saves the stylesheet once while preserving each cell's other formatting:

sheet.CellWrapTextFor(new[] { (Row: 1, Column: 1), (Row: 3, Column: 2) });

Coordinates are 1-based and the complete selection is validated before editing. Pass wrapText: false to clear wrapping for the selected cells.

Tune larger exports

using var document = ExcelDocument.Create("large-report.xlsx");
document.Execution.Mode = ExcelExecutionMode.Automatic;
document.Execution.MaxDegreeOfParallelism = Environment.ProcessorCount;
document.Execution.SaveWorksheetAfterAutoFit = false;

For a new workbook that only contains tabular data, write the XLSX package directly without building an editable workbook model:

using var output = File.Create("large-export.xlsx");

ExcelDocument.WriteRows(
    output,
    rows,
    new[] { "Id", "Name", "Created", "Active" },
    static (writer, row) => writer
        .Write(row.Id)
        .Write(row.Name)
        .Write(row.Created)
        .Write(row.Active),
    new ExcelTabularWriteOptions {
        SheetName = "Data",
        IncludeCellReferences = false,
        UseSharedStrings = false
});

When rows arrive asynchronously, use the same headers and row writer without buffering the sequence:

await ExcelDocument.WriteRowsAsync(
    output,
    GetRowsAsync(cancellationToken),
    new[] { "Id", "Name", "Created", "Active" },
    static (writer, row) => writer
        .Write(row.Id)
        .Write(row.Name)
        .Write(row.Created)
        .Write(row.Active),
    ct: cancellationToken);

WriteRowsAsync awaits and disposes the source enumerator and writes each row once. Because the final row count is unknown when package output starts, this overload does not support CreateTable or AutoFit; add those features through the editable workbook API when they are required.

WriteRows and WriteRowsAsync use inline strings when options are omitted. Pass new ExcelTabularWriteOptions { UseSharedStrings = true } to deduplicate text in a workbook shared-string table. Both methods still consume the source once, including with RequireStreaming = true; memory for shared strings grows with the distinct text values. Pass UseSharedStrings = false for inline text when supplying other options.

Declare styles before writing when rows or columns need default formatting:

var options = new ExcelTabularWriteOptions {
    RequireStreaming = true,
    UseSharedStrings = false,
    Styles = new Dictionary<string, ExcelStyleDefinition> {
        ["Emphasis"] = new() { Bold = true },
        ["Amount"] = new() { NumberFormat = "#,##0.00" },
        ["Plain"] = new()
    },
    DefaultRowStyle = "Emphasis",
    ColumnStyles = new Dictionary<int, string> { [2] = "Amount" }
};

ExcelDocument.WriteRows(output, rows, ["Name", "Amount", "Created", "Comment"],
    static (writer, row) => {
        writer.SetRowStyle(row.Emphasized ? "Emphasis" : null);
        writer.Write(row.Name).Write(row.Amount).Write(row.Created);
        writer.SetNextCellStyle("Plain").WriteBlank();
    }, options);

Styles contains complete definitions based on Normal formatting. Supported properties are bold, italic, underline, font family, size and color, solid background color, text wrapping, horizontal and vertical alignment, and number format. Names are trimmed and compared without case. The catalog is copied and compiled before reading source rows or preparing the destination; callbacks select declared names and cannot register additional styles. The export limits the compiled catalog, including automatic temporal variants, to 64,000 cell formats. Style memory depends on the declared catalog and columns, without buffering rows.

DefaultRowStyle applies to data rows. SetRowStyle must run before the first cell; passing null clears the row default for that row. ColumnStyles uses 1-based schema columns and formats each entire worksheet column, including headers. A row default overrides a column default, and SetNextCellStyle overrides both for one cell. These rules select a complete style, without combining individual properties. An empty definition explicitly selects Normal formatting. The package contains real row and column defaults and assigns the resolved style to authored cells so their formatting is also visible in Excel. Declared columns use the standard Calibri 11 column width when no automatic width was calculated, so applying a column style does not collapse the column. Dates and durations receive automatic number formats that retain the selected visual formatting, unless the definition supplies its own number format. WriteBlank writes a genuine blank; Write((string?)null) retains its empty-text behavior.

The same options apply to WriteRowsAsync and WriteDataReader, including its streaming and materialized paths. Named definitions remain available through GetNamedStyles after loading the exported workbook. For an editable workbook, use document.DefineNamedStyle("Emphasis", new ExcelStyleDefinition { Bold = true }) and the existing sheet.ApplyNamedStyle("Emphasis", "A2:D20") API.

Invalid declarations and pre-cancellation leave the destination untouched. An exception or cancellation after writing starts may leave a partial package; discard or reset it before reuse. The supplied destination stream stays open.

On .NET 8 and later, callers that already have UTF-8 text can pass its bytes to WriteUtf8 without first creating a string:

byte[][] names = [System.Text.Encoding.UTF8.GetBytes("Zażółć 🚀")];
ExcelDocument.WriteRows(output, names, ["Name"],
    static (row, utf8) => row.WriteUtf8(utf8));

The method consumes the bytes during the call, escapes XML markup, and preserves whitespace and carriage returns. It rejects invalid UTF-8 and text longer than Excel's 32,767-character limit. Inline output copies the text directly; selecting shared strings materializes text for the shared-string table.

Fluent compose

using var document = ExcelDocument.Create("composed-report.xlsx");

document.Compose("Report", composer => {
    composer.Title("Demo Report", "Generated with OfficeIMO.Excel");
    composer.Callout("info", "Heads up", "Generated via the fluent API");
    composer.Section("Summary");
    composer.PropertiesGrid(new (string, object?)[] {
        ("Author", "OfficeIMO"),
        ("Date", DateTime.Today.ToString("yyyy-MM-dd"))
    });

    var items = new[] {
        new { Name = "Alice", Score = 90, Status = "OK" },
        new { Name = "Bob", Score = 80, Status = "Warning" }
    };

    composer.TableFrom(items, title: "Scores", visuals: visuals => {
        visuals.NumericColumnDecimals["Score"] = 0;
        visuals.TextBackgrounds["Status"] = new Dictionary<string, string> {
            ["Warning"] = "#FFF3CD"
        };
    });

    composer.HeaderFooter(header => header.Center("Demo Report").FooterRight("Page &P of &N"));
    composer.Finish(autoFitColumns: true);
});

document.Save();

When rows already have a fixed schema, pass a DataTable directly. This keeps its column order and avoids generic object flattening. Fixed-schema tables allow up to 5,000,000 cells by default, including the header row. Set MaxRows, MaxColumns, or MaxCells in the configuration callback when a report needs a different bounded limit. Excel's worksheet row and column limits still apply; select or split data that exceeds them.

using System.Data;
using OfficeIMO.Excel;

var rows = new DataTable("Members");
rows.Columns.Add("Enabled", typeof(bool));
rows.Columns.Add("AD State", typeof(string));
rows.Rows.Add(true, "Enabled");
rows.Rows.Add(false, "Disabled");

using var document = ExcelDocument.Create("members.xlsx");
document.Compose("Members", composer => {
    composer.TableFrom(
        rows,
        title: "Members",
        configure: options => options.Columns = new[] { "Enabled", "AD State" });
    composer.Finish(autoFitColumns: true);
});
document.Save();

Active worksheet

SetActiveWorksheet chooses the worksheet that spreadsheet applications show when the workbook opens. ActiveWorksheet and ActiveWorksheetIndex read it back without building an inspection snapshot.

document.SetActiveWorksheet("Details");
ExcelSheet? active = document.ActiveWorksheet;
int index = document.ActiveWorksheetIndex;

Both resolve the first workbook view's activeTab in workbook-tab order. A missing value selects the first tab, and a value past the last tab selects the last. ActiveWorksheetIndex is relative to Sheets, which contains worksheets only. When the selected tab is a preserved chartsheet or another non-worksheet tab, or the workbook has no worksheets, the getters report null and -1. Hidden worksheets cannot be set active.

Worksheet print areas

Set one or several local A1 selections with ExcelDocument.SetPrintArea. Sheet names containing commas or apostrophes can be quoted; whole-row and whole-column selections are also supported.

document.SetPrintArea(sheet, "B2:D20,F2:H20");
IReadOnlyList<string> areas = sheet.GetPrintAreas();
string? definedNameText = sheet.GetPrintArea();

GetPrintAreas() returns the individual stored references. GetPrintArea() returns the complete defined-name text. The setter validates all areas before replacing the previous selection and rejects references to another worksheet or workbook.

Explicit row heights and AutoFitRow/AutoFitRows results are stored in points. Generated worksheets retain a neutral sheet view so desktop Excel reads those heights consistently. Clearing frozen panes retains other view settings and removes the pane and its selections. Auto-fit no longer inflates stored heights by 1.5; existing stored workbook heights remain unchanged when loaded.

Managed image export

Ranges, worksheets, and workbook batches can be exported as PNG, JPEG, TIFF, lossless WebP, or SVG:

using OfficeIMO.Drawing;

byte[] tiff = sheet.Range("A1:F20").ToTiff(new ExcelImageExportOptions {
    ShowGridlines = false,
    RasterEncoding = new OfficeRasterEncodingOptions {
        Tiff = new OfficeTiffEncodeOptions { Compression = OfficeTiffCompression.PackBits }
    }
});

document.ToImages()
    .ForSheets("Summary", "Data")
    .FitWithin(1600, 1200)
    .AsWebp()
    .Save("preview-images");

sheet.ToImages()
    .UsePrintArea()
    .SplitByManualPageBreaks()
    .WithGridlines(false)
    .AsPng()
    .Save("print-area-pages");

Excel layout, print-title, page-setup, and header/footer composition stays in OfficeIMO.Excel; final sizing and encoding are delegated once to OfficeIMO.Drawing. Worksheet batches can use print-area segments or manual page breaks without routing through a workbook export.

Charts use the same managed image contract and preserve their anchored dimensions:

ExcelChart chart = sheet.ChartFromTable("RevenueTable")
    .RevenueTrend("Revenue trend")
    .Size(640, 320)
    .At(row: 1, column: 5);

chart.ToImage()
    .WithBackground(OfficeColor.White)
    .AsPng()
    .Save("revenue-chart.png");

chart.SaveAsSvg("revenue-chart.svg");

Content provenance

ExcelDocument.InspectProvenance("input.xlsx") reports C2PA and AI-specific IPTC metadata in the workbook and its supported embedded images. ExcelDocument.RemoveProvenance("input.xlsx", "clean.xlsx") removes the selected carriers. Signed-package mutation is blocked unless OfficeSignatureMutationPolicy.RemoveInvalidatedSignatures is selected explicitly. Optional cryptographic C2PA verification remains in OfficeIMO.Security.

When the workbook is already in memory, inspect its encoded package bytes directly. This overload validates the package and inspects supported provenance without accessing the filesystem:

using OfficeIMO.Excel;
using OfficeIMO.Provenance;

static OfficeProvenanceReport InspectUploadedExcel(byte[] packageBytes) =>
    ExcelDocument.InspectProvenance(packageBytes, "upload.xlsx");

Concealed-content inspection and cleanup

ExcelDocument.InspectContentSafety(...) reports hidden sheets/rows/columns, zero geometry, tiny or transparent text, ;;; hidden display formats, resolved low contrast, comments, hidden defined names, drawing runs/fields and ancestor groups, drawing alternative text, and Unicode evidence across XLSX/XLSM/XLSB and supported legacy XLS input. ExcelDocument.RemoveSelectedContent(...) clears only reviewed current payloads, isolates shared-string edits per referencing cell, and writes the original physical format. Conditional-format rendering is diagnosed rather than guessed; signed legacy XLS cleanup fails closed.

Adjacent packages

Package Use it for
OfficeIMO.Excel.Pdf Excel to PDF export through OfficeIMO.Pdf, plus PDF table import to Excel.
OfficeIMO.Excel.GoogleSheets Planning and exporting Excel content to Google Sheets.
OfficeIMO.Excel.Benchmarks Benchmark harness for Excel workloads.

Deeper docs

Targets and license

  • Targets: netstandard2.0, net8.0, net10.0; net472 is included when building on Windows.
  • License: MIT.
  • Repository: EvotecIT/OfficeIMO

VBA source editing

ReadVbaProject() and SetVbaProject(OfficeVbaProject, ...) use the shared native VBA model. They read/edit source without executing it or requiring Office, Python, or a new runtime dependency:

using OfficeIMO;
using OfficeIMO.Excel;

var project = OfficeVbaProject.Create("Automation");
project.AddModule("Helpers",
    "Public Function Value() As Long\r\nValue = 42\r\nEnd Function\r\n");
using var document = ExcelDocument.Create("automation.xlsm");
document.AddWorksheet("Data");
document.SetVbaProject(project);
document.Save();

For an existing workbook, load it, call ReadVbaProject(), edit the detached project, and apply it with SetVbaProject. Workbook document modules retain their base-class identity and must match an existing workbook code name. Worksheet and chart-sheet modules must match exactly one existing sheet code name and host type; the display name is not used to infer a binding. Foreign document classes are rejected. Save in a macro-enabled Open XML format. Source and reference edits preserve unrelated workbook parts and follow the shared model's preservation policy.

Changing a signed VBA project fails by default. Pass new OfficeVbaWriteOptions { AllowSignatureRemoval = true } to remove invalidated VBA signatures explicitly, then re-sign the edited project. An unchanged project keeps its original bytes and signatures. This source API does not edit the VBA carrier in binary .xls or .xlsb files.

Dependency footprint

  • External: Open XML SDK for .xlsx package mechanics. Microsoft BCL/JSON compatibility packages are used on older targets.
  • OfficeIMO: OfficeIMO.Core. The workbook API, BIFF8 .xls reader/writer, large-data paths, validation, and PNG/JPEG/TIFF/WebP/SVG export are first-party.
  • Security: Open XML and VBA signature carriers are inspected and signed-package mutations fail safely without a cryptographic dependency. Package and VBA signature creation and cryptographic validation accept an explicit IOfficeSecurityProvider; OfficeIMO.Security is not pulled transitively.

See the complete OfficeIMO package map for related formats and conversion paths.

ExcelSheet.AddThreadedComments(...) adds a batch of roots and replies in input order. It validates the complete ID/parent plan before appending comments, indexes workbook IDs and author names once, and saves each comment/person part once. Replies must reference an existing root or an earlier root in the batch on the same worksheet and cell. AddThreadedComment(...) uses the same writer for one item.

Generated capability summary

This table is generated from the package-neutral OfficeIMO operation catalog. The detailed source contracts remain authoritative for feature-level behavior and limitations.

Operation Supported Partial Preserved Rejected Unsupported Not applicable
Create 22 13 0 15 1 2
Read 34 0 17 0 0 2
Edit 11 0 39 2 1 0
Preserve 10 0 39 0 0 0
Inspect 6 0 0 0 0 0
Validate 4 0 0 0 0 2
Remove 4 0 0 0 1 0
Convert 49 40 0 8 0 0
Export 5 0 0 0 0 0

The complete rows for OfficeIMO.Excel are published in the generated operation contract.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 is compatible.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (12)

Showing the top 5 NuGet packages that depend on OfficeIMO.Excel:

Package Downloads
OfficeIMO.Excel.Pdf

PDF converter for OfficeIMO.Excel - Export Excel workbooks to PDF using the first-party OfficeIMO.Pdf engine.

Ank.DocToolkit

Convert HTML to PDF and DOCX in C# without a browser - pure managed, no Chromium, no wkhtmltopdf, no native binaries, no LibreOffice, no Office interop. Also converts Markdown to DOCX and PDF; renders DOCX, XLSX and PPTX to PDF; exports DOCX to HTML/Markdown and XLSX to CSV/HTML; reads legacy Word 97-2003 .doc files; reads text out of a PDF; creates/edits DOCX, XLSX and PPTX; password-protects PDF, DOCX, XLSX and PPTX; and inspects and validates digital signatures in DOCX, XLSX and PPTX. Runs on Linux, Windows, macOS and arm64, and makes no network calls at runtime.

OfficeIMO.Reader

Unified, read-only document extraction facade for OfficeIMO (Word/Excel/PowerPoint/Markdown/PDF) intended for AI ingestion.

OfficeIMO.ChartForgeX

Optional typed bridge from ChartForgeX visual artifacts to OfficeIMO Word, Excel, PowerPoint, PDF, drawing, and editable Visio surfaces.

OfficeIMO.Reader.Excel

Excel workbook adapter for OfficeIMO.Reader.Core.

GitHub repositories (2)

Showing the top 2 popular GitHub repositories that depend on OfficeIMO.Excel:

Repository Stars
EvotecIT/EventViewerX
Windows Event Log tooling for PowerShell and .NET: typed queries, reporting, export, WEC, automation, and the PSEventViewer module.
EvotecIT/PSWriteOffice
Moved to OfficeIMO. Development, releases, issues and pull requests: https://github.com/EvotecIT/OfficeIMO
Version Downloads Last Updated
3.5.2 195 10/24/2026
3.5.1 1,648 10/16/2026
3.4.5 1,893 10/10/2026
3.4.4 5,152 9/27/2026
3.4.3 2,742 9/12/2026
3.4.2 848 9/9/2026
3.4.1 705 9/7/2026
3.4.0 593 9/6/2026
3.3.0 698 9/2/2026
3.2.7 2,498 8/29/2026
3.2.6 4,008 8/22/2026
3.2.5 588 8/21/2026
3.2.4 1,534 8/19/2026
3.2.3 946 8/17/2026
3.2.2 3,898 8/13/2026
3.2.1 641 8/11/2026
3.2.0 3,684 8/7/2026
3.1.1 677 8/7/2026
3.1.0 423 8/6/2026
3.0.3 1,528 7/27/2026
Loading failed