From 4e74b727564d3e5da48910a78bc471867ea6352d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vitou=C5=A1?= Date: Thu, 23 Jul 2026 09:12:45 +0200 Subject: [PATCH 1/4] fix: template rendering corrupts workbooks that contain formulas Rendered '\$=' formulas were written as an un-namespaced inside a t=\"inlineStr\" cell, and the regenerated calcChain.xml carried wrong cell references (or none, which is schema-invalid) - either defect makes Excel refuse the output. Fixes the formula-cell serialization, derives calcChain refs from the cell's own address, drops the chain when empty instead of writing an invalid part, and clears collected refs between sheets. --- .../ExcelOpenXmlTemplate.Impl.cs | 13 ++- .../SaveByTemplate/ExcelOpenXmlTemplate.cs | 25 ++--- .../MiniExcelTemplateCalcChainTests.cs | 103 ++++++++++++++++++ 3 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index f075a21f..878bb5b6 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -1006,12 +1006,21 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex) if (!v.InnerText.StartsWith("$=")) continue; - var fNode = c.OwnerDocument.CreateElement("f", Config.SpreadsheetmlXmlns); + // create the f element with the document's prefix: an unprefixed element would rely on a + // default xmlns declaration that CleanXml strips, leaving in no namespace (invalid for Excel) + var fNode = c.OwnerDocument.CreateElement(c.Prefix, "f", Config.SpreadsheetmlXmlns); fNode.InnerText = v.InnerText.Substring(2); c.InsertBefore(fNode, v); c.RemoveChild(v); + // the cell no longer holds an inline string; keeping t="inlineStr" without is invalid + c.RemoveAttribute("t"); - var celRef = ExcelOpenXmlUtils.ConvertXyToCell(ci + 1, rowIndex); + // take the column from the cell's own reference — the position in the child list is wrong + // for sparse rows (cells without content are not emitted) + var columnLetters = new string(c.GetAttribute("r").TakeWhile(char.IsLetter).ToArray()); + var celRef = string.IsNullOrEmpty(columnLetters) + ? ExcelOpenXmlUtils.ConvertXyToCell(ci + 1, rowIndex) + : $"{columnLetters}{rowIndex}"; _calcChainCellRefs.Add(celRef); } } diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs index 646b49a7..640a52df 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs @@ -128,12 +128,18 @@ public void SaveAsByTemplate(Stream templateStream, object value) // disposing writer disposes streams as well. read and parse calc functions before that sheetIdx++; _calcChainContent.Append(CalcChainHelper.GetCalcChainContent(_calcChainCellRefs, sheetIdx)); + _calcChainCellRefs.Clear(); } } // create mode we need to not create first then create here + // The template's own calcChain cannot be reused: row insertion shifts formula cells and its + // entries would point at the old addresses. It is regenerated from the rendered formulas — + // and when none were rendered, dropped entirely, because a calcChain with no entries is + // schema-invalid and Excel rejects the whole package either way. Excel rebuilds the chain + // on open, so dropping it is always safe. var calcChain = outputFileArchive.entries.FirstOrDefault(e => e.FullName.Contains("xl/calcChain.xml")); - if (calcChain != null) + if (calcChain != null && _calcChainContent.Length > 0) { var calcChainPathName = calcChain.FullName; //calcChain.Delete(); @@ -144,23 +150,6 @@ public void SaveAsByTemplate(Stream templateStream, object value) CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString()); } } - else - { - foreach (ZipArchiveEntry entry in originalArchive.Entries) - { - if (entry.FullName.Contains("xl/calcChain.xml")) - { - var newEntry = outputFileArchive.zipFile.CreateEntry(entry.FullName); - - // Copy the content of the original entry to the new entry - using (Stream originalEntryStream = entry.Open()) - using (Stream newEntryStream = newEntry.Open()) - { - originalEntryStream.CopyTo(newEntryStream); - } - } - } - } outputFileArchive.zipFile.Dispose(); } diff --git a/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs new file mode 100644 index 00000000..fed3f100 --- /dev/null +++ b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs @@ -0,0 +1,103 @@ +using System.IO.Compression; +using System.Xml; +using ClosedXML.Excel; +using MiniExcelLibs.Tests.Utils; +using Xunit; + +namespace MiniExcelLibs.Tests.SaveByTemplate; + +/// +/// Regression tests for calcChain.xml handling and '$='-formula cell serialization in template +/// rendering. A template containing any formula carries a calcChain part whose entries point at +/// cell addresses; row insertion shifts formula cells, so the chain must be regenerated from the +/// rendered output — never left stale, and never written empty (a calcChain with zero <c> +/// entries is schema-invalid and Excel refuses to open the whole file). +/// +public class MiniExcelTemplateCalcChainTests +{ + [Fact] + public void TemplateWithStaticFormula_DoesNotWriteStaleOrEmptyCalcChain() + { + // A template with a static Excel formula (below an IEnumerable row) carries a calcChain + // pointing at the formula's pre-render address. After rows are inserted the address is + // stale — the rendered package must not contain a stale or empty calcChain. + using var template = AutoDeletingPath.Create(); + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + ws.Cell("A1").Value = "{{title}}"; + ws.Cell("A3").Value = "{{items.Name}}"; + ws.Cell("B3").Value = "{{items.Qty}}"; + ws.Cell("B5").FormulaA1 = "SUM(B3:B4)"; + wb.SaveAs(template.FilePath); + } + + using var path = AutoDeletingPath.Create(); + MiniExcel.SaveAsByTemplate(path.ToString(), template.FilePath, TwoItemValues()); + + using var zip = ZipFile.OpenRead(path.ToString()); + var calcChain = zip.GetEntry("xl/calcChain.xml"); + if (calcChain != null) + { + using var reader = new StreamReader(calcChain.Open()); + var content = reader.ReadToEnd(); + Assert.Contains(" (two items shift row 7 to 8) with a namespaced child and no inlineStr type + var doc = new XmlDocument(); + using (var sheet = zip.GetEntry("xl/worksheets/sheet1.xml")!.Open()) + doc.Load(sheet); + var ns = new XmlNamespaceManager(doc.NameTable); + ns.AddNamespace("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); + var formulaCell = doc.SelectSingleNode("//x:c[@r='D8']", ns) as XmlElement; + Assert.NotNull(formulaCell); + Assert.Equal("SUM(B5:B6)", formulaCell.SelectSingleNode("x:f", ns)?.InnerText); + Assert.NotEqual("inlineStr", formulaCell.GetAttribute("t")); + + // and the regenerated calcChain points at the formula's real address, not the one derived + // from the cell's position in the row (which would be column B here) + using var chainReader = new StreamReader(zip.GetEntry("xl/calcChain.xml")!.Open()); + var chain = chainReader.ReadToEnd(); + Assert.Contains(@"r=""D8""", chain); + Assert.DoesNotContain(@"r=""B8""", chain); + } + + private static Dictionary TwoItemValues() => new() + { + ["title"] = "FooCompany", + ["items"] = new[] + { + new { Name = "A", Qty = 1 }, + new { Name = "B", Qty = 2 }, + }, + }; +} From 6d05593e1ce6f58be3dca4b6997ec63e47969692 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 24 Jul 2026 21:53:44 +0200 Subject: [PATCH 2/4] Changed the ProcessFormulas method to use the full cell attribute ("r") when not null --- src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index 878bb5b6..99078e67 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -1017,10 +1017,10 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex) // take the column from the cell's own reference — the position in the child list is wrong // for sparse rows (cells without content are not emitted) - var columnLetters = new string(c.GetAttribute("r").TakeWhile(char.IsLetter).ToArray()); - var celRef = string.IsNullOrEmpty(columnLetters) + var rAttr = c.GetAttribute("r"); + var celRef = string.IsNullOrEmpty(rAttr) ? ExcelOpenXmlUtils.ConvertXyToCell(ci + 1, rowIndex) - : $"{columnLetters}{rowIndex}"; + : rAttr; _calcChainCellRefs.Add(celRef); } } From 9ccb447c621d1ee91ebbcc582a39df79f81e6d47 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 24 Jul 2026 22:17:57 +0200 Subject: [PATCH 3/4] Improved calcChain.xml entry's copy logic Added logic to remove calcChain records from [Content_Types].xml and workbook.xml.rels when the calcChain entry must not be copied from the template's archive. --- .../SaveByTemplate/ExcelOpenXmlTemplate.cs | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs index 640a52df..1bd7257c 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Xml; +using System.Xml.Linq; namespace MiniExcelLibs.OpenXml.SaveByTemplate; @@ -81,10 +82,10 @@ public void SaveAsByTemplate(Stream templateStream, object value) // Iterate through each entry in the original archive foreach (ZipArchiveEntry entry in originalArchive.Entries) { - if (entry.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - entry.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - entry.FullName.Contains("xl/calcChain.xml") - ) + if (entry.FullName.TrimStart('/').StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || + entry.FullName.Contains("xl/calcChain.xml") || + entry.FullName.Contains("workbook.xml.rels") || + entry.FullName.Contains("[Content_Types].xml")) continue; // Create a new entry in the new archive with the same name @@ -132,7 +133,21 @@ public void SaveAsByTemplate(Stream templateStream, object value) } } - // create mode we need to not create first then create here + // loading [Content_Types] and workbook.rels for later editing and saving + XDocument wbRelsDoc; + var wbRelsEntry = originalArchive.Entries.First(e => e.FullName.Contains("workbook.xml.rels")); + using (var wbRelsStream = wbRelsEntry.Open()) + { + wbRelsDoc = XDocument.Load(wbRelsStream); + } + + XDocument cTypesDoc; + var cTypesEntry = originalArchive.Entries.First(e => e.FullName.Contains("[Content_Types].xml")); + using (var cTypesStream = cTypesEntry.Open()) + { + cTypesDoc = XDocument.Load(cTypesStream); + } + // The template's own calcChain cannot be reused: row insertion shifts formula cells and its // entries would point at the old addresses. It is regenerated from the rendered formulas — // and when none were rendered, dropped entirely, because a calcChain with no entries is @@ -145,10 +160,31 @@ public void SaveAsByTemplate(Stream templateStream, object value) //calcChain.Delete(); var calcChainEntry = outputFileArchive.zipFile.CreateEntry(calcChainPathName); - using (var calcChainStream = calcChainEntry.Open()) - { - CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString()); - } + using var calcChainStream = calcChainEntry.Open(); + CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString()); + } + else + { + // removing calcChain.xml entry from [Content_Types] and workbook.rels + wbRelsDoc.Root?.Elements() + .FirstOrDefault(x => x.Attribute("Target")?.Value.Contains("calcChain.xml") is true) + ?.Remove(); + + cTypesDoc.Root?.Elements() + .FirstOrDefault(x => x.Attribute("PartName")?.Value.Contains("calcChain.xml") is true) + ?.Remove(); + } + + var newWbRelsEntry = outputFileArchive.zipFile.CreateEntry(wbRelsEntry.FullName); + using (var newWbRelsEntryStream = newWbRelsEntry.Open()) + { + wbRelsDoc.Save(newWbRelsEntryStream); + } + + var newCTypesEntry = outputFileArchive.zipFile.CreateEntry(cTypesEntry.FullName); + using (var newCTypesEntryStream = newCTypesEntry.Open()) + { + cTypesDoc.Save(newCTypesEntryStream); } outputFileArchive.zipFile.Dispose(); From 9a5a060bcac16c13c7fd172926402bef396beb05 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 24 Jul 2026 22:18:42 +0200 Subject: [PATCH 4/4] Minor unrelated changes --- README-NuGet.md | 7 +--- .../ExcelOpenXmlTemplate.Impl.cs | 13 ++++--- .../SaveByTemplate/ExcelOpenXmlTemplate.cs | 21 ++++------- .../MiniExcelTemplateCalcChainTests.cs | 37 ++++++++++++------- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/README-NuGet.md b/README-NuGet.md index 39c03ff8..0dd2546c 100644 --- a/README-NuGet.md +++ b/README-NuGet.md @@ -1,10 +1,5 @@ ## MiniExcel -[![NuGet Version](https://img.shields.io/nuget/v/MiniExcel.svg)](https://www.nuget.org/packages/MiniExcel)  -[![NuGet Downloads](https://img.shields.io/nuget/dt/MiniExcel.svg)](https://www.nuget.org/packages/MiniExcel)  -[![GitHub Stars](https://img.shields.io/github/stars/mini-software/MiniExcel?logo=github)](https://github.com/mini-software/MiniExcel)  -[![Gitee Stars](https://gitee.com/dotnetchina/MiniExcel/badge/star.svg)](https://gitee.com/dotnetchina/MiniExcel)  -[![.NET Version](https://img.shields.io/badge/.NET-%3E%3D%204.5-red.svg)](https://www.nuget.org/packages/MiniExcel)  -[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mini-software/MiniExcel) +[![NuGet Version](https://img.shields.io/nuget/v/MiniExcel.svg)](https://www.nuget.org/packages/MiniExcel) [![NuGet Downloads](https://img.shields.io/nuget/dt/MiniExcel.svg)](https://www.nuget.org/packages/MiniExcel) [![GitHub Stars](https://img.shields.io/github/stars/mini-software/MiniExcel?logo=github)](https://github.com/mini-software/MiniExcel) [![Gitee Stars](https://gitee.com/dotnetchina/MiniExcel/badge/star.svg)](https://gitee.com/dotnetchina/MiniExcel) [![.NET Version](https://img.shields.io/badge/.NET-%3E%3D%204.5-red.svg)](https://www.nuget.org/packages/MiniExcel) [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/mini-software/MiniExcel) This project is part of the [.NET Foundation](https://dotnetfoundation.org/projects/project-detail/miniexcel) and operates under their code of conduct. diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs index 99078e67..93e5548d 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs @@ -1008,19 +1008,21 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex) // create the f element with the document's prefix: an unprefixed element would rely on a // default xmlns declaration that CleanXml strips, leaving in no namespace (invalid for Excel) - var fNode = c.OwnerDocument.CreateElement(c.Prefix, "f", Config.SpreadsheetmlXmlns); + var fNode = c.OwnerDocument!.CreateElement(c.Prefix, "f", Config.SpreadsheetmlXmlns); fNode.InnerText = v.InnerText.Substring(2); c.InsertBefore(fNode, v); c.RemoveChild(v); + // the cell no longer holds an inline string; keeping t="inlineStr" without is invalid c.RemoveAttribute("t"); - // take the column from the cell's own reference — the position in the child list is wrong - // for sparse rows (cells without content are not emitted) + // take the column from the cell's own reference — + // the position in the child list is wrong for sparse rows (cells without content are not emitted) var rAttr = c.GetAttribute("r"); var celRef = string.IsNullOrEmpty(rAttr) ? ExcelOpenXmlUtils.ConvertXyToCell(ci + 1, rowIndex) : rAttr; + _calcChainCellRefs.Add(celRef); } } @@ -1084,7 +1086,8 @@ private static void ReplaceSharedStringsToStr(IDictionary sharedStr c.AppendChild(isNode); c.RemoveAttribute("t"); - c.SetAttribute("t", "inlineStr"); } + c.SetAttribute("t", "inlineStr"); + } } } @@ -1523,4 +1526,4 @@ private static bool EvaluateStatement(object tagValue, string comparisonOperator return false; } -} \ No newline at end of file +} diff --git a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs index 1bd7257c..fee3a091 100644 --- a/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs +++ b/src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.cs @@ -58,6 +58,7 @@ public void SaveAsByTemplate(Stream templateStream, object value) var templateReader = new ExcelOpenXmlSheetReader(templateStream, null); var outputFileArchive = new ExcelOpenXmlZip(_outputFileStream, mode: ZipArchiveMode.Create, true, Encoding.UTF8, isUpdateMode: false); + try { outputFileArchive.entries = templateReader._archive.zipFile.Entries; //TODO:need to remove @@ -71,14 +72,10 @@ public void SaveAsByTemplate(Stream templateStream, object value) { outputFileArchive._entries.Add(entry.FullName.Replace('\\', '/'), entry); } - + templateStream.Position = 0; using (var originalArchive = new ZipArchive(templateStream, ZipArchiveMode.Read)) { - // Create a new zip file for writing - //using (FileStream newZipStream = new FileStream(newZipPath, FileMode.Create)) - //using (ZipArchive newArchive = new ZipArchive(_outputFileStream, ZipArchiveMode.Create)) - // Iterate through each entry in the original archive foreach (ZipArchiveEntry entry in originalArchive.Entries) { @@ -87,7 +84,7 @@ public void SaveAsByTemplate(Stream templateStream, object value) entry.FullName.Contains("workbook.xml.rels") || entry.FullName.Contains("[Content_Types].xml")) continue; - + // Create a new entry in the new archive with the same name var newEntry = outputFileArchive.zipFile.CreateEntry(entry.FullName); @@ -105,9 +102,7 @@ public void SaveAsByTemplate(Stream templateStream, object value) //read all xlsx sheets var templateSheets = templateReader._archive.zipFile.Entries - .Where(w => - w.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - w.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)); + .Where(w => w.FullName.TrimStart('/').StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)); int sheetIdx = 0; foreach (var templateSheet in templateSheets) @@ -151,14 +146,12 @@ public void SaveAsByTemplate(Stream templateStream, object value) // The template's own calcChain cannot be reused: row insertion shifts formula cells and its // entries would point at the old addresses. It is regenerated from the rendered formulas — // and when none were rendered, dropped entirely, because a calcChain with no entries is - // schema-invalid and Excel rejects the whole package either way. Excel rebuilds the chain - // on open, so dropping it is always safe. + // schema-invalid and Excel rejects the whole package either way. + // Excel rebuilds the chain on open, so dropping it is always safe. var calcChain = outputFileArchive.entries.FirstOrDefault(e => e.FullName.Contains("xl/calcChain.xml")); if (calcChain != null && _calcChainContent.Length > 0) { var calcChainPathName = calcChain.FullName; - //calcChain.Delete(); - var calcChainEntry = outputFileArchive.zipFile.CreateEntry(calcChainPathName); using var calcChainStream = calcChainEntry.Open(); CalcChainHelper.GenerateCalcChainSheet(calcChainStream, _calcChainContent.ToString()); @@ -205,4 +198,4 @@ public Task SaveAsByTemplateAsync(Stream templateStream, object value, Cancellat { return Task.Run(() => SaveAsByTemplate(templateStream, value), cancellationToken); } -} \ No newline at end of file +} diff --git a/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs index fed3f100..0efb0763 100644 --- a/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs +++ b/tests/MiniExcelTests/SaveByTemplate/MiniExcelTemplateCalcChainTests.cs @@ -33,7 +33,16 @@ public void TemplateWithStaticFormula_DoesNotWriteStaleOrEmptyCalcChain() } using var path = AutoDeletingPath.Create(); - MiniExcel.SaveAsByTemplate(path.ToString(), template.FilePath, TwoItemValues()); + Dictionary data = new() + { + ["title"] = "FooCompany", + ["items"] = new[] + { + new { Name = "A", Qty = 1 }, + new { Name = "B", Qty = 2 }, + } + }; + MiniExcel.SaveAsByTemplate(path.ToString(), template.FilePath, data); using var zip = ZipFile.OpenRead(path.ToString()); var calcChain = zip.GetEntry("xl/calcChain.xml"); @@ -41,7 +50,7 @@ public void TemplateWithStaticFormula_DoesNotWriteStaleOrEmptyCalcChain() { using var reader = new StreamReader(calcChain.Open()); var content = reader.ReadToEnd(); - Assert.Contains(" data = new() + { + ["title"] = "FooCompany", + ["items"] = new[] + { + new { Name = "A", Qty = 1 }, + new { Name = "B", Qty = 2 }, + } + }; + MiniExcel.SaveAsByTemplate(path.ToString(), template.FilePath, data); using var zip = ZipFile.OpenRead(path.ToString()); // the formula cell: (two items shift row 7 to 8) with a namespaced child and no inlineStr type var doc = new XmlDocument(); using (var sheet = zip.GetEntry("xl/worksheets/sheet1.xml")!.Open()) + { doc.Load(sheet); + } + var ns = new XmlNamespaceManager(doc.NameTable); ns.AddNamespace("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); var formulaCell = doc.SelectSingleNode("//x:c[@r='D8']", ns) as XmlElement; @@ -90,14 +111,4 @@ public void DollarFormulaInSparseRow_WritesValidFormulaCellAndCorrectCalcChainRe Assert.Contains(@"r=""D8""", chain); Assert.DoesNotContain(@"r=""B8""", chain); } - - private static Dictionary TwoItemValues() => new() - { - ["title"] = "FooCompany", - ["items"] = new[] - { - new { Name = "A", Qty = 1 }, - new { Name = "B", Qty = 2 }, - }, - }; }