Skip to content

Deep clone the key and value of a KeyValuePair element [patch] - #91

Merged
matt-edmondson merged 3 commits into
mainfrom
claude/issue-82-keyvaluepair-clone
Sep 29, 2026
Merged

matt-edmondson merged 3 commits into
mainfrom
claude/issue-82-keyvaluepair-clone

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #82

What was wrong

Some sources are sequences of KeyValuePair<TKey, TValue> but aren't statically IDictionary<TKey, TValue>, such as an IReadOnlyDictionary passed to DeepCloneFrom or LINQ over a dictionary. For these, overload resolution picks the element-wise overloads with T = KeyValuePair<,>. The private element helper only cloned IDeepCloneable instances, so the pair was copied as-is. The "clone" then shared its keys and values with the source.

Change

The private DeepClone<T> element helper now rebuilds a KeyValuePair<,> from deep clones of its key and value. This is the lower-risk option from the triage: no new public overloads, so it can't reintroduce the CS0121 ambiguity from #81.

A generic static cache, PairCloner<T>, checks once per element type whether T is a KeyValuePair<,>. If it is, the cache creates a private PairElementCloner<TKey, TValue> with Activator, so no non-public reflection is needed. For every other element type the extra cost is a null check.

Tests

In EnumerableCloneTests:

  • DeepCloneFrom_ReadOnlyDictionarySource_ShouldCloneKeysAndValues: the issue's first repro. Key and value are no longer the source instances.
  • Enumerable_DeepClone_LinqOverDictionary_ShouldCloneKeysAndValues: the issue's second repro, src.Where(...).DeepClone().
  • Enumerable_DeepClone_PairsOfNonCloneables_ShouldKeepTheirContents: pairs of non-cloneables still pass through unchanged.

The first two fail with the fix reverted. The full suite passes (51/51), and the library builds clean for every target framework.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NFZqEcFcZzrKRzQvofupG6

A dictionary reached only as a sequence of pairs, such as an
IReadOnlyDictionary passed to DeepCloneFrom or LINQ over a dictionary,
went through the element-wise overloads with T = KeyValuePair. The
pair never implements IDeepCloneable, so it was copied as-is and the
clone shared its keys and values with the source. The element helper
now rebuilds a KeyValuePair from deep clones of its key and value.

Fixes #82

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFZqEcFcZzrKRzQvofupG6
… reflection [patch]

Binding a private method with BindingFlags.NonPublic tripped Sonar's
accessibility-bypass rule (S3011). A private generic cloner class
created with Activator needs no non-public lookup, and it also moves
the pair-cloning logic into its own type (S3398).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFZqEcFcZzrKRzQvofupG6
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants