Skip to content

Expand "Reflection visible array" constructed promotion to generic interfaces implemented by arrays #127249

@NinoFloris

Description

@NinoFloris

MetadataEETypeNode always promotes reflection-visible array types to MaximallyConstructableType, the rationale being that Array.CreateInstanceFromArrayType might allocate them. This is what makes that API usable without [RequiresDynamicCode].

This rule however doesn't fire for the generic interfaces that arrays implement (IList<T>, ICollection<T>, IEnumerable<T>, IReadOnlyList<T>, IReadOnlyCollection<T>). A library that wants to produce T[] in response to a typeof(IList<T>) entry point can't, because T[]'s MethodTable isn't guaranteed to be emitted.

We're intending to add AOT-friendly support in Npgsql for reading and writing enums without requiring explicit user registrations. Since Postgres pairs every 'base' type with a corresponding array type, we also want to support enum array reading and writing.

Handling these through their underlying type is the recommended AOT-friendly approach for working with unknown enum types. This support can be extended structurally to arrays via Array.CreateInstanceFromArrayType(typeof(TEnum[]), n), using the array cast compatibility to let the underlying type code handle the rest.

Today this works for reader.GetFieldValue<MyEnum[]>() as internally these generic paths do typeof(T) for mapping resolution, which brings the array type up to constructed level through the metadata tier promotion. If a user calls reader.GetFieldValue<IList<MyEnum>>() however, this fails because MyEnum[] was never rooted. None of the workarounds we can apply on our end are satisfying. MakeArrayType requires dynamic code. Per-enum generic registration defeats what convenience the underlying type handling pattern can already give us. And restricting the mappings for this particular case to T[] only loses natural call shapes we support for all other mappings.

The idea would be to root the array type when a reflection-visible closed generic is one of the interfaces arrays implement. With those roots in place Npgsql can safely pair a suppressed MakeArrayType with CreateInstanceFromArrayType to produce an array instance to back the requested interface.

Rooting T[] for every reachable IList<T> (ICollection<T>, and so on) isn't free, but the cost is smaller than the surface suggests. Such reachability in real apps overwhelmingly coexists with T[] already being rooted indirectly via List<T>, ImmutableArray<T>, {ReadOnly}Memory<T>, etc. The rule could also be limited to enum element types, if the unconditional variant would have too wide of an impact. In which case it would just exist to make the underlying type pattern work more uniformly, as if the user explicitly rooted or registered TEnum[].

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions