-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
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 typeAlternative Designs
-
Name-based fallback in
Nullable.GetUnderlyingType— UseNamespace == "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). -
Internal virtual
IsNullableOfTonType— Similar concept but internal-only. Less useful since the community cannot provide implementations for customTypesubclasses.
Risks
- This is a new public virtual method on
Type, which is a widely-subclassed type. The default implementation returnsnullfor 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.