Repository navigation
CompuMaster.EPPlus4: address inherited concurrency defects and document unsupported multithreaded use #26
Description
Current status
The targeted fixes and documentation are merged in PR #27 and published in release 2026.10.07.1. The follow-up investigations are complete. The remaining limitations are documented below and accepted for the current scope; further remediation is deferred unless a concrete use case requires it. Historical reproduction results refer to the pre-fix commit, not the released version.
Problem and scope
CompuMaster.EPPlus4 inherits concurrency defects from EPPlus 4.5.3.3. Concurrent access to the same ExcelPackage can lose or misplace cell values without an exception, corrupt shared style collections, and fail during formula calculation. Working on different worksheets of one workbook does not isolate the shared state.
Until a supported concurrency contract has been implemented and tested, use each package and its workbook, worksheets, ranges, and streams in one thread, including calculation, saving, and disposal. If an application must share a package, it must externally serialize all access with one stable lock per package/workbook. Locks per worksheet or per cell are insufficient. Multi-step read/modify/write sequences require one lock around the entire sequence.
This issue records both the implementation work (part 1) and the documentation work (part 2). It does not claim that every operation on independent packages fails, or that later EPPlus versions have been tested here.
Evidence baseline
- Assessment date: 2026-10-07.
- CompuMaster source:
c18469edbe5662ebb41666219bbef6c1f5338f31. - Runtime probes: freshly built
EPPlus45-FixCalcsEdition.MultiTarget.csproj, targetnet8.0, Windows. The local build succeeded with five existing warnings and no errors. Other target frameworks have not yet been runtime-validated. - Archived upstream baseline:
JanKallman/EPPlusat55c5ba6169eaa6ae7867fefb36a84cc0b28be85a, dated 2020-03-08. CellStore.cs,ExcelStyleCollection.cs,ExcelStyles.cs,ExcelRange.cs,FormulaParsing/ParsingScopes.cs,FormulaParsing/Excel/Functions/Math/Rand.cs, andFormulaParsing/Excel/Operators/OperatorsDict.csare identical to that upstream baseline after namespace and newline/BOM normalization.CalculateExtentions.csis not identical as a whole; the CM implementation still shares the workbook formula parser between worksheet calculations.
Historical reports and maintainer guidance
- Codeplex issue 15154: confirmed historical stream corruption, already fixed upstream before 4.5. The 2015-02-06 fix states that non-thread-safe streams were corrupted while copying to the internal MemoryStream and introduces a static
CopyStreamlock. The CM edition retains that lock. The legacyIssue15154test uses parallel construction/saving, is ignored, and depends on external files; it is not a durable current regression test. Do not count this historical fixed defect as an unresolved current constructor bug. - Upstream issue #330, opened 2018-10-10, still open:
Parallel.Forover a shared worksheet was reported to causeNullReferenceException. Maintainer swmal's response suspected thread safety and retained it as an enhancement, without isolating or confirming the exact cause. A 2019 report addsInvalidCastException(Decimal to ExcelColumn) inExcelStyles.SetStyleAddress/CellStore.SetRangeValueSpecial. - EPPlusSoftware issue #894: different worksheets of one workbook were reported to cause exceptions. Jan Källman's 2023-06-06 response explicitly recommends avoiding multiple threads on one workbook. This is later-version guidance, not a direct EPPlus 4.5 runtime reproduction.
- EPPlusSoftware issue #1611, 2024: tracks better multithreading support for ranges, styles, and calculation and references #894. Do not infer EPPlus 4.5 support or a backported fix from this later-version roadmap.
Reproduced defects at the historical pre-fix baseline
1. CellStore: concurrent writes can lose data or throw
The probe writes 300 rows in each of 64 columns on one worksheet. Each write obtains a fresh ws.Cells[row, col], so this does not depend on sharing a cached range object.
- Initial parallel run with maximum degree 16: 5 exceptions, including
NullReferenceExceptioninCellStore.GetPosition/SetValueSpecial; 5,578 wrong/missing cells out of 19,200. This mismatch count includes writes skipped after a worker's exception. - Second parallel run with default scheduling: 92 wrong/missing cells out of 19,200, with zero exceptions. This demonstrates silent incorrect results.
- Serial control: zero mismatches and zero exceptions.
CellStore.SetValue and other writes lock _columnIndex. AddColumn replaces that array when it grows. Threads can therefore lock different objects for the same store. GetValue also reads without that lock. The exact interleaving behind each observed mismatch has not been isolated, but the changing lock identity is a concrete synchronization defect.
2. Shared styles: different worksheet locks do not protect workbook collections
The probe initializes workbook styles before parallel work, then sets 500 distinct number formats on each of eight worksheets.
- Both parallel runs failed, with 8 worker exceptions. A representative exception is
InvalidOperationException: concurrent changes to a non-concurrent collection corrupted its state. - Stack:
Dictionary.ContainsKey->ExcelStyleCollection.FindByID->ExcelXfs.GetIdNumberFormat->ExcelStyles.SetStyleAddress. - Mismatch counts were 3,923/4,000 and 3,830/4,000; these include unperformed operations after workers failed and must not all be described as silent corruption.
- Serial control: zero mismatches and zero exceptions.
ExcelStyles.PropertyChange locks the individual worksheet's _values, while style collections are shared by the workbook. ExcelStyleCollection.Add changes a List and Dictionary without a common collection lock; ContainsKey plus Add and ID assignment are not atomic. Reads, additions, IDs, and enumeration must be considered together.
3. Calculation: different worksheets share mutable workbook parser state
The probe prepares eight worksheets with 300 formulas each (A<row>*2+1) and calculates the first worksheet before parallel work to initialize the parser.
- Both parallel runs failed with 4 worker exceptions. Observed exceptions include
FormatException(operator missing expression) andNullReferenceExceptionduring formula-token storage. - Wrong/missing result counts were 1,171/2,400 and 831/2,400, including incomplete calculations after exceptions.
- Serial control: zero mismatches and zero exceptions.
Worksheet calculation uses worksheet.Workbook.FormulaParser. InitNewCalc resets provider state, and ParsingScopes uses an unprotected Stack. Parser/provider/graph state is shared. Runtime failure is confirmed; the complete causal interleaving remains to be isolated.
4. Cached ExcelRange: indexing changes the shared object's address
ExcelRange indexers mutate address fields and return this. A coordinated two-thread probe uses var cells = ws.Cells; thread A selects A1, thread B selects B1, and thread A then assigns through its earlier reference. The assignment lands in B1, leaving A1 empty.
ws.Cells itself creates a new ExcelRange on each property access. Distinguish this shared-range hazard from defect 1, which reproduces with fresh ranges. Locking the indexer and setter separately cannot protect the entire select-then-use sequence. Changing historical indexer semantics may break compatibility and must not be done silently.
Additional code observations at the historical pre-fix baseline
ExcelWorkbook.Styles, the workbook parser, and other lazy initialization paths need a synchronization audit; unprotected check-then-create can publish competing instances. The runtime probes above deliberately preinitialize styles/parser where relevant.ExcelAddressCache.Add,GetNewId, andClearlock, butGetperforms unprotectedContainsKeythen index access, andCountis unprotected. Concurrent reads/clear can race. No separate runtime reproduction was performed.RAND()changes a static Seed without synchronization and uses millisecond-based seeding. Lost updates and correlated/repeated random sequences are plausible across independent packages. No statistical/runtime validation was performed.- The static
ExcelPackage._id++used for image URIs is not atomic. The image lock is package-local. A shared counter race does not by itself prove image-part collisions across independent packages because part namespaces are package-local; investigate without overstating impact. OperatorsDict.Instanceand several operator/validation singletons use unsynchronized lazy initialization. Duplicate initialization alone is not proof of incorrect results; the publicly exposed mutable dictionary needs contract review.CopyStreamuses a static global lock: affected stream copies on independent packages are serialized. This is a code-established scalability concern, not a newly confirmed correctness failure.Seekoccurs outside that lock, so it is not a comprehensive shared-stream safety guarantee.- Concurrent save/dispose, structural changes, and nominally read-only access have not been comprehensively validated and must remain outside any new guarantee until explicitly reviewed/tested.
Independent-package control and limits
100 independently owned packages were created, formatted, calculated, saved to individual memory buffers, and reopened in parallel. Both runs had zero exceptions and zero result mismatches. The serial control also passed. This supports independent package/stream ownership as the safer concurrency boundary, but does not establish comprehensive thread safety for all features, target frameworks, images, encryption, RAND(), or first-use initialization.
Part 1: completed implementation and regression coverage
- Give CellStore a stable monitor for competing reads/writes, resizing, traversal steps, and disposal. Cell traversal is not a snapshot under arbitrary concurrent structural edits.
- Protect workbook-wide style updates and collection lookups/ID creation; protect core lazy style initialization and capture style-collection membership for enumeration.
- Serialize workbook/worksheet/range/formula calculations, including dependency-chain and token preparation, and coordinate parser-manager operations under the package monitor.
- Protect core package/workbook/worksheet/style/parser first-use publication and coordinate calculation, saving, and disposal. Arbitrary workbook edits still require caller coordination.
- Add 20 durable synchronization regression cases, including serial controls, result/round-trip assertions, controlled interleavings, stable monitor identity, cached-range compatibility, and independent-package overlap.
- Validate net48 and net8.0 at runtime: the selected suite has 99 passed and 2 skipped per runtime. Build all four library targets and pass CI on Windows, Linux, and macOS.
- Preserve public signatures and cached-range semantics; document caller responsibility for complete select/read/modify/write sequences.
- Fix address-cache read/clear races, operator-dictionary publication, and the non-atomic image counter; classify other observations as unresolved investigation or documented limitations.
- Document the targeted synchronization scope and remaining unsupported operations without claiming general thread safety.
- Merge PR Fix internal EPPlus4 races and document threading limits #27 and publish release 2026.10.07.1 with successful post-merge and release workflows.
Part 2: completed README and NuGet documentation
- Add matching
Known Issuessections afterSpecial CompuMaster Editionin all three README entry points, including the packaged NuGet README. - Use the warning: Epplus4 (including its fork CompuMaster.EPPlus4) is not thread-safe.
- Recommend one owner thread per package, link Jan Källman's guidance, and explain why worksheet/cell locks are insufficient.
- Distinguish the fixed internal races from remaining cached-range, compound-operation, callback-reentry, cross-workbook/direct-mutation, and global stream-copy limitations.
- Verify the released package README content, all four EPPlus4 target assemblies, and dependency versions. All eleven NuGet uploads were accepted.
Completed investigations and accepted remaining limitations
- Investigate the shared
RAND()seed across independent packages and determine whether a compatibility-preserving fix is needed. The released-assembly probes confirmed repeated output sequences even during sequential use; see the investigation comment for evidence and limits. - Audit remaining lazy initialization and publicly mutable operator/validation paths; classify any additional demonstrated defects and add targeted regression coverage if needed.
- Evaluate safe recursive calculation/parser use from custom functions or loggers separately. Internal monitor reentrancy is implemented, but recursive calculation/save/dispose from callbacks remains unsupported; waiting for another thread requiring the same package can deadlock.
Follow-up investigation on the published 2026.10.7.1 assemblies confirmed RAND quality defects, Encryption/Properties first-use failures, and grouped-formula/logger callback reentry failures on net48 and net8.0. Detailed evidence and a self-contained harness are in the latest investigation comment. The checked investigation items above indicate completed investigation, not remediation. The possible follow-up changes below are deferred and are not active TODO items.
- Deferred: improve RAND/RANDBETWEEN generator quality.
- Deferred: protect Encryption/Properties publication and retained settings/metadata during unsupported shared-package concurrency.
- Deferred: resolve remaining singleton publication and clarify the mutable global operator/token contract.
- Deferred: guard unsupported callback/lifecycle reentry; true nested-context support remains a separate design decision.
Cached range mutation and complete caller-side ownership/coordination remain intentional compatibility constraints. The inherited global CopyStream lock remains a documented throughput limitation. Neither implies an outstanding promise to provide general multithreading support in this release. Benchmarks were explicitly excluded from this task.
The targeted implementation, documentation, release work, and follow-up investigations are complete. At the maintainer's request, this issue is closed and removed from the active TODO list. The remaining findings are retained as documented limitations; further fixes are deferred until a concrete use case warrants them. Each package should continue to have one owner thread, and the RAND limitation also applies to sequential use. Historical evidence is retained for traceability.
Self-contained reproduction
Save the following program as Program.cs in a net8.0 console project referencing the freshly built CompuMaster.EPPlus4 assembly. Run normally for the parallel probes and with serial for the serial controls. The cached-range probe deliberately coordinates competing accesses and runs only in parallel mode. Counts vary with scheduling. This is an exploratory probe, not a replacement for durable regression tests.
Program.cs
using CompuMaster.Epplus4;
using CompuMaster.Epplus4.FormulaParsing;
using System.Collections.Concurrent;
class Program
{
static bool Serialized;
static void For(int from, int to, Action<int> action) { if(Serialized) { for(int i=from;i<to;i++) action(i); } else Parallel.For(from,to,action); }
static void Main(string[] args)
{
Serialized = args.Contains("serial"); Console.WriteLine("Serialized="+Serialized);
Console.WriteLine("Assembly: " + typeof(ExcelPackage).Assembly.FullName);
if(!Serialized) CachedRange();
Values();
Styles();
Calculation();
IndependentPackages();
}
static void Failure(string label, ConcurrentQueue<Exception> errors)
{
Console.WriteLine(label + " exceptions=" + errors.Count);
if (errors.TryPeek(out var e)) Console.WriteLine(e.ToString());
}
static void CachedRange()
{
using var p = new ExcelPackage();
var ws = p.Workbook.Worksheets.Add("Sheet");
var cells = ws.Cells;
using var selectedA = new ManualResetEventSlim();
using var selectedB = new ManualResetEventSlim();
var t = new Thread(() => { var a = cells[1, 1]; selectedA.Set(); selectedB.Wait(); a.Value = "A"; });
t.Start(); selectedA.Wait(); var b = cells[1, 2]; selectedB.Set(); t.Join();
Console.WriteLine("Cached range: expected A1=A; actual A1=" + (ws.Cells[1,1].Value ?? "<null>") + "; B1=" + ws.Cells[1,2].Value);
}
static void Values()
{
using var p = new ExcelPackage();
var ws = p.Workbook.Worksheets.Add("Sheet");
var errors = new ConcurrentQueue<Exception>();
For(1, 65, col => {
try { for (int row=1; row<=300; row++) ws.Cells[row,col].Value = row * 1000 + col; }
catch(Exception e) { errors.Enqueue(e); }
});
int wrong = 0;
for(int col=1;col<=64;col++) for(int row=1;row<=300;row++) if(!Equals(ws.Cells[row,col].Value, row * 1000 + col)) wrong++;
Console.WriteLine("Same sheet fresh ranges: mismatches=" + wrong + "/19200"); Failure("Values", errors);
}
static void Styles()
{
using var p = new ExcelPackage();
var sheets = Enumerable.Range(0,8).Select(i => p.Workbook.Worksheets.Add("Sheet" + i)).ToArray();
var unused = p.Workbook.Styles;
var errors = new ConcurrentQueue<Exception>();
For(0,8, i => {
try { for(int row=1;row<=500;row++) sheets[i].Cells[row,1].Style.Numberformat.Format = "0.00\"s" + i + "r" + row + "\""; }
catch(Exception e) { errors.Enqueue(e); }
});
int wrong=0;
for(int i=0;i<8;i++) for(int row=1;row<=500;row++) {
try { if(sheets[i].Cells[row,1].Style.Numberformat.Format != "0.00\"s" + i + "r" + row + "\"") wrong++; }
catch(Exception e) { errors.Enqueue(e); wrong++; }
}
Console.WriteLine("Different sheets same workbook: style mismatches=" + wrong + "/4000"); Failure("Styles",errors);
}
static void Calculation()
{
using var p = new ExcelPackage();
var sheets=Enumerable.Range(0,8).Select(i=>p.Workbook.Worksheets.Add("Sheet"+i)).ToArray();
for(int i=0;i<8;i++) for(int row=1;row<=300;row++) { sheets[i].Cells[row,1].Value = i+row; sheets[i].Cells[row,2].Formula = "A"+row+"*2+1"; }
// Initialize parser before parallel use to isolate shared parsing state from lazy initialization.
sheets[0].Calculate();
var errors=new ConcurrentQueue<Exception>();
For(0,8,i=>{ try{sheets[i].Calculate();} catch(Exception e){errors.Enqueue(e);} });
int wrong=0;
for(int i=0;i<8;i++) for(int row=1;row<=300;row++) {
var value=sheets[i].Cells[row,2].Value;
if(value is not double d || d != (i+row)*2+1) wrong++;
}
Console.WriteLine("Different sheets calculation: mismatches=" + wrong + "/2400"); Failure("Calculation",errors);
}
static void IndependentPackages()
{
var errors=new ConcurrentQueue<Exception>(); int wrong=0;
For(0,100,i=>{
try {
using var p=new ExcelPackage(); var ws=p.Workbook.Worksheets.Add("Sheet");
ws.Cells[1,1].Value=i; ws.Cells[1,2].Formula="A1*2+1"; ws.Cells[1,1].Style.Numberformat.Format="0.00";
ws.Calculate(); var bytes=p.GetAsByteArray();
using var p2=new ExcelPackage(new MemoryStream(bytes));
if(Convert.ToDouble(p2.Workbook.Worksheets[0].Cells[1,2].Value) != i*2+1) Interlocked.Increment(ref wrong);
} catch(Exception e){errors.Enqueue(e);}
});
Console.WriteLine("Independent packages roundtrip: mismatches="+wrong+"/100"); Failure("Independent packages",errors);
}
}
Activity
jochenwezel commentedon Oct 7, 2026
Implementation is available for review in draft PR #27 (commit 001141f). It fixes the reproduced cell-store, workbook-style, and concurrent calculation races, adds a shared package monitor for calculation/parser-manager operations/save/dispose, protects core first-use publication and address-cache reads, safely publishes OperatorsDict, and makes the image counter atomic.
All 20 new regression cases pass on net48 and net8.0. Together with the selected existing EPPlus4 engine/data tests, each runtime has 99 passed and 2 skipped tests. All four library targets build. The packaged README was checked against its source and all three Known Issues sections match. No benchmarks or release/package publication were performed.
The remaining scope is explicit in the READMEs: cached range indexers still mutate and return the same object; arbitrary workbook reads/writes and compound operations require caller coordination; callbacks must not recursively calculate/parse/save/dispose the same package or wait for another thread needing its monitor; cross-workbook style copying and direct XML/collection mutation require external coordination. Cell enumeration is not a snapshot. The inherited global CopyStream lock and shared RAND seed remain unchanged; other lazy/operator/validation paths are not comprehensively validated.
The monitor supports internal nested method calls. It does not implement the checklist's broader proposal to make recursive calculation from custom functions safe; that remains unsupported and documented. Passing these regression cases establishes the targeted fixes, not general thread safety. This issue remains open pending review and integration, with the additional observations retained as follow-up items.
jochenwezel commentedon Oct 7, 2026
PR #27 has been merged into main and released as 2026.10.07.1.
The post-merge build/test workflow succeeded on Windows, Linux, and macOS. The release workflow also succeeded, attached six binary archives, and uploaded all eleven NuGet packages (normalized package version 2026.10.7.1). The generated packages contain the updated EPPlus4 dependency, all four EPPlus4 target assemblies, and the updated NuGet README. NuGet accepted the uploads; public package-index propagation is still pending at verification time.
This issue remains open for the documented remaining limitations and investigation items. The release fixes the targeted internal races but does not establish general thread safety; packages still require one owner thread or complete external coordination.
jochenwezel commentedon Oct 7, 2026
Results of the three remaining investigations
Checked the actual published
CompuMaster.EPPlus4assemblies, version 2026.10.7.1, on Windows with .NET Framework 4.8 and .NET 8.0.31. No production code was changed. These are correctness probes, not performance benchmarks. Counts depending on thread scheduling are examples, not guaranteed frequencies.1. RAND: confirmed poor randomness, including independent packages
Rand.Executeupdates a process-wide static Seed without synchronization, then constructs a newRandom(DateTime.Now.Millisecond + Seed)for every result. Sequential execution cycles through seed offsets 0, 5, ..., 55, so the sequential algorithm has at most 1,055 possible integer seed inputs. The parallel update reads the property multiple times and is not an atomic read/modify/write; lost-update frequencies were not isolated separately.All values stayed in [0, 1), but the extensive exact repetition demonstrates a correctness/quality issue already in sequential use.
RANDBETWEENdelegates toRandand inherits this generator weakness. Making only the small seed increment atomic would not solve the low diversity. A suitable fix is a stateful generator with adequate initial seeding and explicitly synchronized ownership, compatible with all supported frameworks.Source: Rand.cs.
2. Remaining lazy initialization and mutable global objects
Two concrete first-use defects were reproduced through normal public access on fresh packages, without reflection or cached-field resets:
package.Encryptionsimultaneously; multiple returned instances can exist. After all threads finish, assigning a password to a returned instance that was overwritten in the cache does not configure the retained instance. In 100 fresh-package trials this happened 14 times on net8.0 and once on net48; those packages saved as ordinary unencrypted ZIP/XLSX data. The probe uses a synthetic password. This involves the already unsupported shared-package concurrency case, not correctly owned independent packages.OfficeProperties, creates package parts, and loads separate XML documents. Eight competing getters over 100 fresh packages produced 524/800 getter exceptions on net8.0 and 393/800 on net48. A representative exception isInvalidOperationException: Part already exist, fromZipPackage.CreatePart -> OfficeProperties.GetXmlDocument -> OfficeProperties constructor -> ExcelWorkbook.Properties. Updating a discarded returned object's Title was absent from the retained object in 57/100 and 96/100 trials respectively.Source: Encryption getter, Properties getter.
Additional cold-publication replay probes reset only private cached fields between rounds via reflection, to simulate repeated first-use races. They produced competing instances for
Operator.Percent,ExpressionConverter.Instance,ExcelDataValidationType.List, workbook Protection/View, and package Compatibility. This is not evidence that all of them corrupt workbook results: operators/converter are effectively stateless, validation descriptors have private setters, and several wrappers refer to shared XML/package state. The independent-package list-validation probe (100 rounds, eight packages per round) passed; a wrong exported validation result was not reproduced. Validation descriptors also compare Type in Equals but use object identity in GetHashCode, and two internal List comparisons use reference identity; these are additional contract concerns when duplicate descriptors are published.OperatorsDict.Instanceis safely initialized after PR #27, but its public dictionary remains mutable. Temporarily replacing its '+' mapping with Minus changes a parser's2+3result from 5 to -1. The defaultITokenSeparatorProvider.Tokensexposes one process-wide dictionary containing mutable Token objects. Appending to the returned '+' token affected a second provider and changed2+3to 2 without an exception. These deliberate mutation probes demonstrate cross-instance shared state; they do not show failures in normal use with unchanged defaults. Mutation during parallel parsing remains unsupported. TokenSeparatorProvider's static constructor itself is safely published.Suggested fixes: protect remaining package/workbook first-use publication under the existing package monitor, beginning with Encryption and Properties; safely publish immutable singleton descriptors; explicitly define the global dictionary/token customization boundary without silently breaking existing public mutation APIs. This was a scoped audit of the identified Package/Workbook, FormulaParsing, and DataValidation paths, not a guarantee for every workbook feature or lazy path (for example VBA/load/dispose interactions).
3. Callback reentry: confirmed calculation errors and lifecycle hazards
On both runtimes:
(CALLBACK()+10)*2, with CALLBACK returning 7, produced 34 and its dependent cell produced 35.FormulaParserManager.Parse("2+3"),Calculate(otherWorksheet, "2+3"), orCalculate(otherWorksheet), the outer result and dependent cell became #VALUE!. Inner calculation returned 5 where applicable.(SUM(1,2)+10)*2from 26 to #VALUE!.CALLBACK()+10andSUM(CALLBACK(),10)controls and nested variants happened to pass. Do not extrapolate these passing cases into safe general reentry.NullReferenceException; calling save twice duringCALLBACK()+SUM(1,2)+CALLBACK()produced #VALUE! in the outer calculation.Cause: monitor reentrancy permits the nested entry but does not isolate the parser, builder graph, compiler expression field, dependency tokens, or provider state. For example ExpressionGraphBuilder.Build resets the active graph, and ExpressionCompiler.Compile replaces shared compiler state.
The narrow next step is an explicit, consistent guard/diagnostic for unsupported reentry and lifecycle operations during active calculation. Truly supporting nested calculations requires isolated calculation contexts and broader regression coverage. Waiting on another thread for the same package must remain prohibited by the ownership contract.
Implementation work remaining
The original three investigations are now completed at this scope. The fixes above are not implemented or released yet, so this issue should remain open. Existing one-thread-per-package guidance still applies, and RAND is an additional concern even under that guidance.
Reproduction harness
For a fresh reproduction, create a console project targeting net48/net8.0 with LangVersion latest and a PackageReference to
CompuMaster.EPPlus4version2026.10.7.1, and use the following Program.cs. Run modesrand,encryption,properties,controls,mutable,validation,lazy, andlogger. Runcallback "(CALLBACK()+10)*2"for the grouped reentry failure;callbackuses the simpler passing case.lazyandvalidationexplicitly use reflection only to replay cached singleton first use;encryption,properties, RAND, and callback/logger failure probes do not. The watchdog exits a stalled probe after 30 seconds.Program.cs
jochenwezel commentedon Oct 7, 2026
Closing at the maintainer's request to remove this issue from the active TODO list. The targeted fixes, regression coverage, README/NuGet warnings, release 2026.10.07.1, and follow-up investigations are complete. Remaining findings are retained as documented limitations; additional remediation is deferred until a concrete use case requires it. This closure does not indicate general thread safety or resolution of every finding. Continue to use each ExcelPackage in one owner thread; the RAND/RANDBETWEEN quality limitation also applies to sequential use.