From bfb3df6a756222a3a10aec9c70be9e91ced85f00 Mon Sep 17 00:00:00 2001
From: swmal <897655+swmal@users.noreply.github.com>
Date: Tue, 24 Feb 2026 10:59:39 +0100
Subject: [PATCH 1/2] #2260 - Added CancellationToken to Calculate
---
src/EPPlus/ExcelPackage.cs | 3 +
src/EPPlus/ExcelWorkbook.cs | 31 ++++
.../FormulaParsing/CalculateExtensions.cs | 69 +++++++--
.../DependencyChain/RpnFormulaExecution.cs | 42 +++++
.../FormulaParsing/ExcelCalculationOption.cs | 23 +++
.../FormulaParsing/CancelCalculationTests.cs | 146 ++++++++++++++++++
6 files changed, 298 insertions(+), 16 deletions(-)
create mode 100644 src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs
diff --git a/src/EPPlus/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs
index 9df1801012..073a202a0b 100644
--- a/src/EPPlus/ExcelPackage.cs
+++ b/src/EPPlus/ExcelPackage.cs
@@ -954,6 +954,9 @@ public void Dispose()
public void Save()
{
CheckNotDisposed();
+#if !NET35
+ Workbook.ThrowIfCalculationCancelled();
+#endif
try
{
if (_stream is MemoryStream && _stream.Length > 0)
diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs
index 276d191569..58b592cbeb 100644
--- a/src/EPPlus/ExcelWorkbook.cs
+++ b/src/EPPlus/ExcelWorkbook.cs
@@ -402,6 +402,37 @@ private void GetSharedStrings()
}
+ #region Calculation cancellation (poison flag)
+
+ #if !NET35
+ internal bool IsCalculationCancelled { get; private set; }
+
+ internal void MarkCalculationCancelled()
+ {
+ IsCalculationCancelled = true;
+ }
+
+ internal void ThrowIfCalculationCancelled()
+ {
+ if (IsCalculationCancelled)
+ {
+ throw new InvalidOperationException(
+ "This workbook has been left in an inconsistent state due to a cancelled " +
+ "calculation. The workbook must be disposed and cannot be used for further " +
+ "operations. Reload the workbook from the source to continue.");
+ }
+ }
+
+ ///
+ /// Returns true if a calculation was cancelled, leaving the workbook in an inconsistent state.
+ /// A workbook in this state must be disposed — saving or recalculating is not permitted.
+ ///
+ public bool IsCalculationInconsistent => IsCalculationCancelled;
+ #endif
+
+ #endregion
+
+
internal void GetDefinedNames()
{
XmlNodeList nl = WorkbookXml.SelectNodes("//d:definedNames/d:definedName", NameSpaceManager);
diff --git a/src/EPPlus/FormulaParsing/CalculateExtensions.cs b/src/EPPlus/FormulaParsing/CalculateExtensions.cs
index 1af3e851d7..89f4353895 100644
--- a/src/EPPlus/FormulaParsing/CalculateExtensions.cs
+++ b/src/EPPlus/FormulaParsing/CalculateExtensions.cs
@@ -62,6 +62,10 @@ public static void Calculate(this ExcelWorkbook workbook, ActionCalculation options
public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption options)
{
+ #if !NET35
+ workbook.ThrowIfCalculationCancelled(); // Guard: prevent recalc on poisoned workbook
+ #endif
+
Init(workbook);
var filterInfo = new FilterInfo(workbook);
@@ -74,13 +78,25 @@ public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption
}
//CalcChain(workbook, workbook.FormulaParser, dc, options);
- var dc=RpnFormulaExecution.Execute(workbook, options);
- dc._parsingContext.RangeCriteriaCache?.Clear();
- if (workbook.FormulaParser.Logger != null)
+#if !NET35
+ try
{
- var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count);
- workbook.FormulaParser.Logger.Log(msg);
+#endif
+ var dc =RpnFormulaExecution.Execute(workbook, options);
+ dc._parsingContext.RangeCriteriaCache?.Clear();
+ if (workbook.FormulaParser.Logger != null)
+ {
+ var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count);
+ workbook.FormulaParser.Logger.Log(msg);
+ }
+#if !NET35
}
+ catch (OperationCanceledException)
+ {
+ workbook.MarkCalculationCancelled();
+ throw;
+ }
+#endif
}
internal static RpnOptimizedDependencyChain CalculateWithDC(this ExcelWorkbook workbook, Action configHandler)
{
@@ -158,8 +174,21 @@ public static void Calculate(this ExcelWorksheet worksheet, Action
@@ -195,15 +224,23 @@ public static void Calculate(this ExcelRangeBase range, ActionCalculation options
public static void Calculate(this ExcelRangeBase range, ExcelCalculationOption options)
{
- Init(range._workbook);
- //var parser = range._workbook.FormulaParser;
- //var filterInfo = new FilterInfo(range._workbook);
- //parser.InitNewCalc(filterInfo);
- //var dc = DependencyChainFactory.Create(range, options);
- //CalcChain(range._workbook, parser, dc, options);
- var dc = RpnFormulaExecution.Execute(range, options);
- // Clear RangeCriteriaCache after calculation completes
- dc._parsingContext.RangeCriteriaCache?.Clear();
+#if !NET35
+ range._workbook.ThrowIfCalculationCancelled();
+ try
+ {
+#endif
+ Init(range._workbook);
+ var dc = RpnFormulaExecution.Execute(range, options);
+ // Clear RangeCriteriaCache after calculation completes
+ dc._parsingContext.RangeCriteriaCache?.Clear();
+#if !NET35
+ }
+ catch (OperationCanceledException)
+ {
+ range._workbook.MarkCalculationCancelled();
+ throw;
+ }
+#endif
}
///
diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
index a9b6b114fb..42e29e4c85 100644
--- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
+++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
@@ -46,6 +46,9 @@ internal static RpnOptimizedDependencyChain Execute(ExcelWorkbook wb, ExcelCalcu
var depChain = new RpnOptimizedDependencyChain(wb, options);
foreach (var ws in wb.Worksheets)
{
+#if !NET35
+ options.CancellationToken.ThrowIfCancellationRequested();
+#endif
if (ws.IsChartSheet == false)
{
ExecuteChain(depChain, ws.Cells, options, true);
@@ -118,9 +121,15 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang
{
var ws = range.Worksheet;
RpnFormula f = null;
+#if !NET35
+ var ct = options.CancellationToken; // Cache locally — avoids property lookup in hot loop
+#endif
var fs = new CellStoreEnumerator