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
ReadOnlyDictionary.DeepClone() drops the wrapped dictionary's key comparer: a case-insensitive source's clone can't find "KEY" #87
DeepClone(this ReadOnlyDictionary<TKey,TValue>) (DeepClone/DeepCloneContainerExtensions.cs:241-246) builds the inner dictionary with CloneDictionary(source, source). CloneDictionary (line ~286) chooses the comparer from the runtime type of its first argument, and it only recognises Dictionary<,> and SortedDictionary<,>. Here the first argument is the ReadOnlyDictionary wrapper, so the default arm always runs (_ => new Dictionary<TKey, TValue>()). The comparer of the dictionary being wrapped is never checked.
Cloning through IReadOnlyDictionary<,> (line 268) has the same problem.
This is not covered by #83, which lists ConcurrentDictionary, SortedList and the immutable dictionaries. Adding arms for those types to CloneDictionary would still leave the wrapper case broken, because the wrapper's own type is what gets inspected.
Observed on current main (net10.0): True / False / False. Expected: True / True / True.
If the wrapped dictionary uses a comparer that is stricter than the default (for example ReferenceEqualityComparer with two equal but distinct keys), the clone throws ArgumentException: An item with the same key has already been added. This is the same failure #83 describes for the other types.
Why it matters
A read-only view over a case-insensitive dictionary is a common way to expose configuration or lookup tables. The clone looks correct, but lookups that work on the source fail on the clone, and the missing key only shows up later.
Suggested fix / acceptance criteria
In the ReadOnlyDictionary overload, and in CloneDictionary when the source is a ReadOnlyDictionary<,>, look at the wrapped dictionary rather than the wrapper. ReadOnlyDictionary exposes it only through the protected Dictionary property, so either:
read it once through a cached reflection accessor and pass it to CloneDictionary as the object to inspect, or
if reflection isn't acceptable, document in the <remarks> that the clone of a ReadOnlyDictionary always uses the default comparer.
Add a test: a ReadOnlyDictionary over an OrdinalIgnoreCase dictionary clones to one where ContainsKey("KEY") is true, through both the concrete and the IReadOnlyDictionary overloads.
Priority: Medium. The clone of a ReadOnlyDictionary silently falls back to the default comparer, so a lookup that works on the source fails on the clone. It can also throw when the source's comparer is stricter than the default. There is a workaround: clone the inner dictionary yourself.
Area / suggested owner:DeepCloneContainerExtensions, in the ReadOnlyDictionary / IReadOnlyDictionary overloads and in CloneDictionary.
Notes: Reading the wrapped dictionary through a cached reflection accessor keeps the behavior correct. The option that only documents the limitation should be the fallback, not the first choice.
Activity
matt-edmondson commentedon Sep 28, 2026
Triage
ReadOnlyDictionarysilently falls back to the default comparer, so a lookup that works on the source fails on the clone. It can also throw when the source's comparer is stricter than the default. There is a workaround: clone the inner dictionary yourself.DeepCloneContainerExtensions, in theReadOnlyDictionary/IReadOnlyDictionaryoverloads and inCloneDictionary.CloneDictionarypicks the comparer. No open PR covers any of them.Generated by Claude Code