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
Deep cloning a ConcurrentDictionary, SortedList or ImmutableDictionary through IDictionary drops its key comparer: lookups start failing, and a reference-equality source throws ArgumentException #83
CloneDictionary (DeepClone/DeepCloneContainerExtensions.cs:227-237) keeps the source's key comparer only when the runtime type is Dictionary<,> or SortedDictionary<,>. Every other IDictionary<TKey,TValue> / IReadOnlyDictionary<TKey,TValue> falls through to the default arm:
_ =>newDictionary<TKey,TValue>(),// line 234: default comparer, default kind
The #77 fix covered only those two types. Other common dictionaries also expose their comparer, but it is thrown away:
ImmutableDictionary<,>.KeyComparer and ImmutableSortedDictionary<,>.KeyComparer
The clone therefore compares keys differently from the source:
Lookups silently fail when the source comparer is coarser than the default (for example OrdinalIgnoreCase).
Cloning throws when the source comparer is finer than the default. For example, with ReferenceEqualityComparer and two equal but distinct keys, AddClonedPairs (line 249) calls dest.Add on a default-comparer Dictionary, and that throws on the duplicate.
usingSystem.Collections.Concurrent;usingSystem.Collections.Immutable;usingktsu.DeepClone;varcd=newConcurrentDictionary<string,int>(StringComparer.OrdinalIgnoreCase);cd["Key"]=1;varc1=((IDictionary<string,int>)cd).DeepClone();Console.WriteLine($"{cd.ContainsKey("key")}{c1.ContainsKey("key")}");varsl=newSortedList<string,int>(Comparer<string>.Create((x,y)=>string.CompareOrdinal(y,x))){["a"]=1,["c"]=3,["b"]=2};varc2=((IDictionary<string,int>)sl).DeepClone();c2["d"]=4;sl["d"]=4;Console.WriteLine($"{string.Join(",",sl.Keys)} | {c2.GetType().Name}{string.Join(",",c2.Keys)}");varim=ImmutableDictionary.Create<string,int>(StringComparer.OrdinalIgnoreCase).Add("Key",1);Console.WriteLine(((IDictionary<string,int>)im).DeepClone().ContainsKey("key"));varrc=newConcurrentDictionary<object,int>(ReferenceEqualityComparer.Instance);rc[newstring('k',1)]=1;rc[newstring('k',1)]=2;// Count == 2((IDictionary<object,int>)rc).DeepClone();// throws// The same data in a Dictionary<object,int>(ReferenceEqualityComparer) clones fine (Count 2).
ConcurrentDictionary: source['key'] found=True, clone['key'] found=False
SortedList: source keys=d,c,b,a | clone type=Dictionary`2, clone keys=c,b,a,d
ImmutableDictionary: source['key'] found=True, clone['key'] found=False
ArgumentException: An item with the same key has already been added. Key: k
Expected: each clone finds "key" as its source does, the SortedList clone stays sorted with the source's comparer, and the reference-equality clone has 2 entries rather than throwing.
Why it matters
A deep clone should look up keys the same way its source does. Because the call succeeds and returns a dictionary that looks correct, the failure shows up later as missing keys. The reference-equality case turns a valid source into a runtime exception. #77 fixed this for Dictionary and SortedDictionary, and the other BCL dictionaries with comparers still have the same bug.
Suggested fix / acceptance criteria
Add arms to CloneDictionary for types that expose a comparer. Each arm should create the same kind of dictionary (or a mutable one with the same comparer):
ConcurrentDictionary<K,V> c becomes new ConcurrentDictionary<K,V>(c.Comparer), under #if NET5_0_OR_GREATER because Comparer does not exist on netstandard2.x.
SortedList<K,V> s becomes new SortedList<K,V>(s.Count, s.Comparer)
For ImmutableDictionary / ImmutableSortedDictionary, return a mutable Dictionary / SortedDictionary built with KeyComparer, or build through the immutable type's builder. Either way, keep the comparer.
Add tests for each type that check the comparer is kept (case-insensitive lookup works, a ReferenceEqualityComparer source with equal keys clones without throwing, and a SortedList clone keeps its order and comparer).
Update the <remarks> on both DeepClone overloads (lines ~129-131 and ~200) so they list the types whose comparer is kept.
What's wrong
CloneDictionary(DeepClone/DeepCloneContainerExtensions.cs:227-237) keeps the source's key comparer only when the runtime type isDictionary<,>orSortedDictionary<,>. Every otherIDictionary<TKey,TValue>/IReadOnlyDictionary<TKey,TValue>falls through to the default arm:The #77 fix covered only those two types. Other common dictionaries also expose their comparer, but it is thrown away:
ConcurrentDictionary<,>.Comparer(.NET 5+). The README lists ConcurrentDictionary as supported, andSpecializedCollectionTests.ConcurrentDictionary_DeepClone_ShouldCreateIndependentCopyshows casting toIDictionaryas the intended call, because of the CS0121 ambiguity in dictionary.DeepClone() fails to compile (CS0121 ambiguous call) for Dictionary, SortedDictionary and ConcurrentDictionary, although the XML doc example shows that call #81.SortedList<,>.ComparerImmutableDictionary<,>.KeyComparerandImmutableSortedDictionary<,>.KeyComparerThe clone therefore compares keys differently from the source:
OrdinalIgnoreCase).ReferenceEqualityComparerand two equal but distinct keys,AddClonedPairs(line 249) callsdest.Addon a default-comparerDictionary, and that throws on the duplicate.SortedListcomes back as an unsortedDictionary, so it stops keeping order on later inserts. This is the same problem Dictionary DeepClone() drops the source's key comparer, so a cloned case-insensitive dictionary stops finding keys #77 fixed forSortedDictionary.Reproduction
Observed (net10.0, current main 3d31b66):
Expected: each clone finds
"key"as its source does, theSortedListclone stays sorted with the source's comparer, and the reference-equality clone has 2 entries rather than throwing.Why it matters
A deep clone should look up keys the same way its source does. Because the call succeeds and returns a dictionary that looks correct, the failure shows up later as missing keys. The reference-equality case turns a valid source into a runtime exception. #77 fixed this for
DictionaryandSortedDictionary, and the other BCL dictionaries with comparers still have the same bug.Suggested fix / acceptance criteria
CloneDictionaryfor types that expose a comparer. Each arm should create the same kind of dictionary (or a mutable one with the same comparer):ConcurrentDictionary<K,V> cbecomesnew ConcurrentDictionary<K,V>(c.Comparer), under#if NET5_0_OR_GREATERbecauseComparerdoes not exist on netstandard2.x.SortedList<K,V> sbecomesnew SortedList<K,V>(s.Count, s.Comparer)ImmutableDictionary/ImmutableSortedDictionary, return a mutableDictionary/SortedDictionarybuilt withKeyComparer, or build through the immutable type's builder. Either way, keep the comparer.ReferenceEqualityComparersource with equal keys clones without throwing, and aSortedListclone keeps its order and comparer).<remarks>on bothDeepCloneoverloads (lines ~129-131 and ~200) so they list the types whose comparer is kept.