Skip to content

Under NativeAOT, DeepClone over KeyValuePair elements with a value-type key or value throws TypeInitializationException (regression from #82) #92

Open

Description

@matt-edmondson

What's wrong

The fix for #82 (PR #91) deep clones KeyValuePair<TKey, TValue> elements through a cloner that it builds with reflection (DeepClone/DeepCloneContainerExtensions.cs, PairCloner<T>, around lines 143–153):

internal static IElementCloner<T>? Instance { get; } =
    typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(KeyValuePair<,>)
        ? (IElementCloner<T>)Activator.CreateInstance(typeof(PairElementCloner<,>).MakeGenericType(typeof(T).GetGenericArguments()))!
        : null;

Under NativeAOT, MakeGenericType cannot produce an instantiation over value types unless the compiler already generated it.

  • Building the library with -p:IsAotCompatible=true reports IL3050 at that line.
  • The failure happens in a static initializer, so it surfaces as TypeInitializationException. Every later clone of that element type in the process then fails the same way.

Repro (NativeAOT, linux-x64, net10.0)

  • new List<KeyValuePair<int, int>> { new(1, 2) }.DeepClone().ToList() → TypeInitializationException
  • dest.DeepCloneFrom((IReadOnlyDictionary<int, Foo>)dict) into a List<KeyValuePair<int, Foo>> → TypeInitializationException
  • List<KeyValuePair<string, Foo>> works. Both type arguments are reference types, so the AOT compiler can fall back to shared canonical code.

Before #91 these calls returned the pairs without cloning them. For AOT and trimmed consumers, the shallow-copy bug has become a crash.

Suggested fix

Clone pairs without MakeGenericType:

  • Add typed overloads so the compiler binds pair sequences statically:

    • DeepClone<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>>)
    • DeepCloneFrom<TKey, TValue>(this ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>)

    Each clones pair.Key and pair.Value directly. The existing dictionary overloads are more specific, so they still win overload resolution. Check that the new overloads don't reintroduce the CS0121 ambiguities tracked in sortedList.DeepClone() and immutableDictionary.DeepClone() don't compile (CS0121 ambiguous between the IDictionary and IReadOnlyDictionary overloads) #86.

  • Drop the reflection path, or keep it only for pairs reached through an open generic T, guarded by RuntimeFeature.IsDynamicCodeSupported, with the shallow behaviour as the fallback.

Acceptance:

Activity

  1. matt-edmondson commented on Sep 29, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Suggested next step: add typed KeyValuePair<TKey, TValue> overloads and drop or guard the MakeGenericType path. Add an IsAotCompatible=true build to CI so this can't regress silently.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions