You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DeepCloneFrom from a ConcurrentDictionary under concurrent writes throws ~40% of the time and sometimes adds a phantom (default, default) entry #100
varcd=newConcurrentDictionary<int,int>();for(inti=1;i<=1000;i++)cd[i]=i;// key 0 never exists// writer thread: loop { k = 1 + rnd.Next(2000); if (!cd.TryRemove(k, out _)) cd[k] = k; }vardest=newDictionary<int,int>();dest.DeepCloneFrom(cd);// IDictionary overload// and: new List<KeyValuePair<int,int>>().DeepCloneFrom(cd); // ICollection overload
Observed:
IDictionary overload, 63,217 runs: 23,197 threw the CopyTo ArgumentException; 17,817 threw An item with the same key has already been added. Key: 0 (two padding slots); 4,685 succeeded with a key 0 that was never in the source.
cd.DeepClone() and ((IDictionary<,>)cd).DeepClone() had no errors in the same run.
Expected: no exception and no entries that were never in the source.
Why it matters
Concurrent writers are the reason to use ConcurrentDictionary. With value-type keys the clone silently contains wrong data; with reference-type keys the padding surfaces as ArgumentNullException.
Suggested fix
Buffer with an explicit loop (List<T> items = []; foreach (T x in source) items.Add(x);). Avoid ToArray(), new List<T>(source) and spreads, which all take the same Count/CopyTo shortcut. Optionally special-case source is ConcurrentDictionary<K,V> c and use c.ToArray() for an atomic snapshot. With the explicit loop applied locally, the same probe ran 49,026 times with 0 phantom entries and 0 throws.
Acceptance: a stress test cloning from a concurrently mutated ConcurrentDictionary never throws and never yields a key absent from the source; the #64 same-instance guarantee still holds.
Priority: High. DeepCloneFrom on a ConcurrentDictionary with active writers throws about 40% of the time. Worse, it silently adds (default, default) entries that never existed in the source, and the issue's stress run produced thousands of such phantom entries. Concurrent writers are the main reason to use ConcurrentDictionary.
Suggested area / assignment:DeepClone/DeepCloneContainerExtensions.cs lines 48 and 84, the collection-expression snapshots
Recommended next step: Replace both [.. source] snapshots with an explicit foreach into a List<T>, and optionally special-case ConcurrentDictionary with ToArray(). Add the stress test from the issue, with a bounded run time, as a regression guard.
Activity
matt-edmondson commentedon Oct 6, 2026
Triage
DeepCloneFromempties the destination whendestandsourceare the same collection instance #64)DeepCloneFromon aConcurrentDictionarywith active writers throws about 40% of the time. Worse, it silently adds(default, default)entries that never existed in the source, and the issue's stress run produced thousands of such phantom entries. Concurrent writers are the main reason to useConcurrentDictionary.DeepClone/DeepCloneContainerExtensions.cslines 48 and 84, the collection-expression snapshotsDeepCloneFromempties the destination whendestandsourceare the same collection instance #64 snapshot change, andDeepCloneFromempties the destination whendestandsourceare the same collection instance #64's same-instance guarantee must be kept. It is not a duplicate.Recommended next step: Replace both
[.. source]snapshots with an explicitforeachinto aList<T>, and optionally special-caseConcurrentDictionarywithToArray(). Add the stress test from the issue, with a bounded run time, as a regression guard.Generated by Claude Code