Uh oh!
There was an error while loading. Please reload this page.
Add Type.GetNullableUnderlyingType() virtual API - #126905
Conversation
Add a new public virtual Type.GetNullableUnderlyingType() method that returns the underlying type T for Nullable<T>, or null otherwise. Nullable.GetUnderlyingType() now forwards to this virtual method. This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType(), enabling Type subclasses like MetadataLoadContext's RoType to provide correct implementations. Changes: - Type.cs: New virtual with ReferenceEquals default (works for RuntimeType) - Nullable.cs: Forward GetUnderlyingType to the new virtual - RoType.cs: Override using CoreType.NullableT identity comparison - RuntimeType.Mono.cs: Update IsNullableOfT to use new virtual - System.Runtime.cs: Add API to ref assembly - NullableTests.cs: Tests for both RuntimeType and MLC paths All 24 NullableTests + 267 NullabilityInfoContextTests pass. Fixesdotnet#124216 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Base virtual now throws NotSupportedException(SR.NotSupported_SubclassOverride) instead of falling back to ReferenceEquals check (matches IsByRefLike pattern) - Add override to RuntimeType (shared) with the ReferenceEquals logic - Add override to RuntimeType.NativeAot.cs with the same logic - Add TypeDelegator override forwarding to typeImpl.GetNullableUnderlyingType() - Add TypeDelegator entry to System.Runtime ref assembly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Type.GetNullableUnderlyingType() virtual APIThere was a problem hiding this comment.
Pull request overview
This PR introduces a new public virtual Type.GetNullableUnderlyingType() API so non-RuntimeTypeType providers (notably MetadataLoadContext’s RoType) can correctly identify closed Nullable<T> types, and updates Nullable.GetUnderlyingType(Type) to delegate to this new virtual.
Changes:
- Add
Type.GetNullableUnderlyingType()and implement/override it forRuntimeType(CoreCLR/Mono), NativeAOTRuntimeType,TypeDelegator, andMetadataLoadContext’sRoType. - Change
Nullable.GetUnderlyingType(Type)to forward toType.GetNullableUnderlyingType(). - Add System.Runtime tests covering both
RuntimeTypeandMetadataLoadContextbehavior, plus a test project reference toSystem.Reflection.MetadataLoadContext.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Type.cs | Adds the new public virtual GetNullableUnderlyingType() method. |
| src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs | Overrides GetNullableUnderlyingType() for runtime types. |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs | Adds NativeAOT override of GetNullableUnderlyingType(). |
| src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs | Forwards the new virtual through TypeDelegator. |
| src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs | Implements nullable detection for MLC RoType. |
| src/libraries/System.Private.CoreLib/src/System/Nullable.cs | Updates Nullable.GetUnderlyingType to delegate to the new virtual. |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Updates the ref assembly surface area for the new API and TypeDelegator override. |
| src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs | Switches Mono’s internal nullable check to use GetNullableUnderlyingType(). |
| src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs | Adds tests for Type.GetNullableUnderlyingType() and MLC scenarios. |
| src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj | Adds test-time project reference to System.Reflection.MetadataLoadContext. |
Copilot's findings
- Files reviewed: 10/10 changed files
- Comments generated: 3
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Use GetType(..., throwOnError: true) for clearer failure messages - Add Assert.Same(intType, underlying) and Assert.NotSame(typeof(int), underlying) to verify the returned type is the MLC-projected type, not a runtime type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…iveAOT/Mono - Remove shared RuntimeType.cs override; add per-runtime overrides instead - CoreCLR: use TypeHandle.IsNullable + InstantiationArg0() fast path, fallback to GetGenericTypeDefinition() == typeof(Nullable<>) for open generic / non-MethodTable cases - NativeAOT: use _pUnderlyingEEType->NullableType fast path, fallback to GetGenericTypeDefinition() == typeof(Nullable<>) - Mono: use GetGenericTypeDefinition() ReferenceEquals path (no MethodTable access) for compat (virtual omits it per jkotas feedback) - Add GC.KeepAlive(this) in CoreCLR after raw pointer use Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ve MLC tests - TypeBuilderInstantiation: return null (avoids breaking callers of Nullable.GetUnderlyingType on Emit-instantiated types) - SignatureConstructedGenericType: return null (same reason) - ModifiedType: delegate to _unmodifiedType.GetNullableUnderlyingType() - SignatureModifiedType: delegate to _unmodifiedType.GetNullableUnderlyingType() - Fix TypeDelegator ref assembly entry placement: move to the methods section (alphabetically after GetNestedTypes, before GetProperties) - Fix CoreCLR implementation: cache AsMethodTable() result in local pMT to avoid double-call and improve clarity - Move MLC tests from System.Runtime.Tests/NullableTests.cs to System.Reflection.MetadataLoadContext/tests/TypeTests.Nullable.cs; use TestUtils.GetPathToCoreAssembly() instead of RuntimeEnvironment.GetRuntimeDirectory() - Remove MLC ProjectReference from System.Runtime.Tests.csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new virtual Type.GetNullableUnderlyingType() so non-RuntimeType implementations (notably MetadataLoadContext’s RoType) can correctly detect Nullable<T>, and updates Nullable.GetUnderlyingType(Type) to forward to the virtual.
Changes:
- Introduces
Type.GetNullableUnderlyingType()and wiresNullable.GetUnderlyingType()to call it. - Implements overrides for CoreCLR, Mono, NativeAOT
RuntimeType, and key wrapper types (TypeDelegator, modified/signature types). - Adds tests for
RuntimeTypeandMetadataLoadContextbehavior.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Type.cs | Adds new virtual GetNullableUnderlyingType() API. |
| src/libraries/System.Private.CoreLib/src/System/Nullable.cs | Forwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType(). |
| src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs | Implements CoreCLR RuntimeType override using MethodTable fast-path + fallback. |
| src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs | Implements Mono RuntimeType override; updates IsNullableOfT to use it. |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs | Implements NativeAOT RuntimeType override. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs | Forwards the new virtual to the underlying Type. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs | Forwards the new virtual to the unmodified type. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs | Forwards the new virtual to the unmodified type. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs | Explicitly returns null for the new virtual. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs | Explicitly returns null for the new virtual. |
| src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs | Implements MLC RoType override via CoreType.NullableT identity. |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Adds the new API to the ref assembly and TypeDelegator override surface. |
| src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs | Adds tests for Type.GetNullableUnderlyingType() on runtime types. |
| src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs | Adds MLC-specific tests for nullable detection, including open-generic case. |
| src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj | Includes the new MLC test file in compilation. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs:99
- The new
GetNullableUnderlyingTypetest cases don't cover the open generic definitiontypeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case assertingtypeof(Nullable<>).GetNullableUnderlyingType()returnsnullto prevent regressions (and to catch the current behavior in the runtime overrides).
[Theory]
[InlineData(typeof(int?), typeof(int))]
[InlineData(typeof(int), null)]
[InlineData(typeof(G<int>), null)]
public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected)
{
Assert.Equal(expected, type.GetNullableUnderlyingType());
}
- Files reviewed: 15/15 changed files
- Comments generated: 4
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…tiation behavior - Use IsConstructedGenericType (not IsGenericType) in NativeAOT and Mono overrides to correctly return null for the open generic typeof(Nullable<>) instead of incorrectly returning type parameter T - Fix TypeBuilderInstantiation.GetNullableUnderlyingType to return the type argument when _genericType is typeof(Nullable<>), preserving the behavior that existed via Nullable.GetUnderlyingType before this change - Add [InlineData(typeof(Nullable<>), null)] to the RuntimeType theory test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… in NullableTests.cs
…t NET GetNullableUnderlyingType() is new in .NET 11. The MLC library multi-targets net11.0, net10.0, netstandard2.0, and netfx. Using #if NET caused CS0115 (no suitable method found to override) when building for net10.0, since #if NET is true for net10.0 but Type.GetNullableUnderlyingType() doesn't exist there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new public virtual Type.GetNullableUnderlyingType() API so non-RuntimeType implementations (notably MetadataLoadContext’s RoType) can correctly recognize closed Nullable<T> and provide the underlying T. Nullable.GetUnderlyingType(Type) is updated to delegate to this virtual, mirroring the existing Enum.GetUnderlyingType() → Type.GetEnumUnderlyingType() pattern.
Changes:
- Introduce
Type.GetNullableUnderlyingType()(virtual) and wireNullable.GetUnderlyingType(Type)to call it for constructed generic types. - Implement/forward the virtual across CoreCLR, Mono, NativeAOT, MetadataLoadContext (
RoType), and common wrapper types (TypeDelegator, modified/signature types,TypeBuilderInstantiation). - Add tests covering both
RuntimeTypeandMetadataLoadContextbehavior.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Type.cs | Adds the new public virtual GetNullableUnderlyingType() API and docs. |
| src/libraries/System.Private.CoreLib/src/System/Nullable.cs | Updates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType(). |
| src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs | CoreCLR override that recognizes Nullable<T> via method table fast-path and fallback. |
| src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs | Mono override implementation + internal IsNullableOfT updated to use the new virtual. |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs | NativeAOT override implementation. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs | Forwards the new virtual to the delegated typeImpl. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs | Forwards the new virtual to the underlying unmodified type. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs | Forwards the new virtual to the underlying unmodified type. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs | Sealed override returning null to preserve existing signature-type semantics. |
| src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs | Adds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations. |
| src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs | Adds RoType override (guarded) using core-type identity comparison for Nullable<T>. |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Updates ref surface area for Type and TypeDelegator. |
| src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs | Adds direct Type.GetNullableUnderlyingType() runtime tests. |
| src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs | Adds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call. |
| src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj | Includes the new MLC test file in the test project. |
Copilot's findings
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
SignatureModifiedType also overrides this method to surface the unmodified type's Nullable<T> behavior, so the previous comment was inaccurate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added When you commit this breaking change:
Tagging @dotnet/compat for awareness of the breaking change. |
…riable When Nullable<T> is constructed over a generic type parameter (e.g. typeof(Nullable<>).MakeGenericType(typeof(MyStruct<>).GetGenericArguments()[0])), the resulting MethodTable has IsNullable but InstantiationArg0() returns a TypeDesc, not a MethodTable*. Casting that to MethodTable* and feeding it to RuntimeTypeHandle.GetRuntimeTypeFromHandle trips the Fall back to managed GetGenericArguments()[0] whenever the Nullable<T> contains generic variables (covers both the open Nullable<> definition and Nullable<ABC> over a generic parameter). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Revert RuntimeTypeInfo.GetNullableUnderlyingType to virtual returning null; add narrow override on NativeFormatRuntimeNamedTypeInfo for typeof(Nullable<>). - Add ref emit tests covering TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, and TypeBuilderInstantiation overrides. - Add SignatureConstructedGenericType and SignatureModifiedType tests via Type.MakeGenericSignatureType and Type.MakeModifiedSignatureType. - Add ModifiedType tests using a function-pointer-return holder to obtain a ModifiedType wrapping Nullable<int>. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The MetadataLoadContext RoModifiedType.GetGenericTypeDefinition() throws NotSupportedException, which caused the base RoType.GetNullableUnderlyingType to fail on modified Nullable<T> instances. Mirror the runtime ModifiedType override so the modified generic argument is returned instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The base Type.GetNullableUnderlyingType throws NotSupportedException by design so subclass authors must opt in. SymbolType (returned by TypeBuilder.MakeArrayType/MakePointerType/MakeByRefType) needs to override the new virtual to return null. Add tests covering Nullable.GetUnderlyingType on each SymbolType variant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AaronRobinsonMSFT
commented
Apr 28, 2026
Note This comment was generated with assistance from GitHub Copilot. Filed the breaking-change documentation issue: dotnet/docs#53407. Remaining checklist item from the policy bot above:
|
Uh oh!
There was an error while loading. Please reload this page.
Closes#125388
Fixes#124216
Breaking change documentation: dotnet/docs#53407
Summary
Adds a new public virtual
Type.GetNullableUnderlyingType()method so thatTypesubclasses (e.g.MetadataLoadContext'sRoType) can correctly identifyNullabletypes.Nullable.GetUnderlyingType()now forwards to this virtual.This follows the same pattern as
Enum.GetUnderlyingType()forwarding toType.GetEnumUnderlyingType().Contract
Type.GetNullableUnderlyingType()returns a non-nullresult for both:Nullable<T>(returnsT), andtypeof(Nullable<>)(returns the generic type parameterT).Nullable.GetUnderlyingType(Type)continues to returnnullfor the generic type definition for COMPAT.Type.GetNullableUnderlyingType()throwsNotSupportedException. CustomTypesubclasses outside the BCL must override it; this is a behavioral breaking change documented in [Breaking change]: Nullable.GetUnderlyingType throws on custom Type subclasses that don't override Type.GetNullableUnderlyingType docs#53407.Changes
Public API
Type.cs: Newpublic virtual Type? GetNullableUnderlyingType()that throwsNotSupportedException(SR.NotSupported_SubclassOverride)(matchesIsByRefLikepattern per @MichalStrehovsky's feedback). XML doc documents that the open genericNullable<>is treated as nullable and yields the generic type parameter.System.Runtime.cs/System.Reflection.Emit.cs(ref assemblies): New API +TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilderoverrides.Nullable.GetUnderlyingTyperewireNullable.cs: Now delegates to the new virtual after preserving theIsGenericTypeDefinitionCOMPAT short-circuit.Runtime overrides (all three runtimes)
RuntimeType.CoreCLR.cs/RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. OpenNullable<>returnsGetGenericArguments()[0]since the native fast-path can't yield aMethodTablefor the formal type parameterT.RuntimeType.NativeAot.cs: Same handling for constructedNullable<X>via the EEType fast-path.RuntimeTypeInfo.cs(NativeAOT): Addedpublic virtualreturningnull(per @jkotas's feedback).NativeFormatRuntimeNamedTypeInfo.cs(NativeAOT): Sealed override that returns the generic parameter only when the type istypeof(Nullable<>).RuntimeConstructedGenericTypeInfo.cs(NativeAOT): override for constructed generics.Reflection subclasses
TypeDelegator.cs: Override forwarding totypeImpl.GetNullableUnderlyingType().SignatureType.cs/SignatureConstructedGenericType.cs/SignatureModifiedType.cs: Overrides that delegate through the generic definition.ModifiedType.cs: Override delegating through the unmodified type.Reflection.Emit
TypeBuilder.cs/EnumBuilder.cs/GenericTypeParameterBuilder.cs/TypeBuilderInstantiation.cs: Overrides returningnull(or appropriate result for instantiations).SymbolType.cs: Override returningnullsoNullable.GetUnderlyingTypedoesn't throw onMakeArrayType/MakePointerType/MakeByRefTyperesults fromTypeBuilder.MetadataLoadContext
RoType.cs: Override usingCoreType.NullableTidentity comparison; usesGetGenericArguments()[0]so the openNullable<>returns the MLC-projected generic parameter rather than indexing emptyGenericTypeArguments.RoModifiedType.cs: Override delegating through the unmodified type (required becauseRoModifiedType.GetGenericTypeDefinition()throws).Tests
NullableTests.cs: Coverage forRuntimeType(constructed + open-generic) andTypeDelegator.SignatureTypes.cs: Coverage forSignatureConstructedGenericTypeandSignatureModifiedType.ModifiedTypeTests.cs: NewNullableModifiedTypeHolder(usesvolatile delegate*<int?>to obtain aModifiedTypewrappingNullable<int>) and tests for modified Nullable / non-Nullable.TypeBuilderGetNullableUnderlyingType.cs(new): Coverage forTypeBuilder,EnumBuilder,GenericTypeParameterBuilder,TypeBuilderInstantiation, andSymbolType(Array / multi-dim Array / Pointer / ByRef).TypeTests.Nullable.cs(MetadataLoadContext): Coverage forRoType(constructed + open-generic).Note
This PR description was updated with assistance from GitHub Copilot.