Skip to content

[API Proposal]: Type.GetNullableUnderlyingType() #125388

@AaronRobinsonMSFT

Description

@AaronRobinsonMSFT

Background and motivation

Nullable.GetUnderlyingType(Type) currently uses ReferenceEquals(genericType, typeof(Nullable<>)) to identify nullable types. This only works for RuntimeType instances — it fails for types from MetadataLoadContext (and any other Type subclass), returning null even for valid Nullable<T> types.

This is the same class of problem that Enum.GetUnderlyingType had before Type.GetEnumUnderlyingType() was added — a static helper that only works with runtime types, with no way for custom Type implementations to provide correct behavior.

The cascading impact is significant: NullabilityInfoContext calls Nullable.GetUnderlyingType() at three callsites, so MetadataLoadContext types get completely wrong nullability analysis (see #124216 for details).

API Proposal

namespace System;

public abstract class Type
{
    /// <summary>
    /// Returns the underlying type argument of a <see cref="Nullable{T}"/> type.
    /// </summary>
    /// <returns>
    /// The type argument if the current type is a closed generic Nullable{T};
    /// otherwise, null.
    /// </returns>
    public virtual Type? GetNullableUnderlyingType();
}

Nullable.GetUnderlyingType(Type) would then forward to the new virtual:

public static Type? GetUnderlyingType(Type nullableType)
{
    ArgumentNullException.ThrowIfNull(nullableType);
    return nullableType.GetNullableUnderlyingType();
}

API Usage

// Works the same as before for runtime types:
Type? underlying = typeof(int?).GetNullableUnderlyingType(); // System.Int32
typeof(int).GetNullableUnderlyingType(); // null

// Now also works for MetadataLoadContext types:
using var mlc = new MetadataLoadContext(resolver);
Assembly asm = mlc.LoadFromAssemblyName("System.Runtime");
Type mlcNullableInt = asm.GetType("System.Nullable`1")!.MakeGenericType(asm.GetType("System.Int32")!);
Type? underlying = mlcNullableInt.GetNullableUnderlyingType(); // the MLC Int32 type

// Nullable.GetUnderlyingType forwards to the virtual, so it also works:
Nullable.GetUnderlyingType(mlcNullableInt); // the MLC Int32 type

Alternative Designs

  1. Name-based fallback in Nullable.GetUnderlyingType — Use Namespace == "System" && Name == "Nullable\1"` as a fallback. Rejected because it would match user-defined types with the same name (raised by @jkotas in Fix Nullable.GetUnderlyingType for MetadataLoadContext types #125356).

  2. Internal virtual IsNullableOfT on Type — Similar concept but internal-only. Less useful since the community cannot provide implementations for custom Type subclasses.

Risks

  • This is a new public virtual method on Type, which is a widely-subclassed type. The default implementation returns null for non-Nullable<T> types, which is safe for existing subclasses.
  • Matches the established precedent of Type.GetEnumUnderlyingType().

Proposed implementation

See PR #125356 for a complete working implementation with tests.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions