Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Type.GetNullableUnderlyingType() virtual API - #126905

Merged
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype
Apr 29, 2026
Merged

Add Type.GetNullableUnderlyingType() virtual API#126905
AaronRobinsonMSFT merged 24 commits into
dotnet:mainfrom
AaronRobinsonMSFT:fix/124216-nullable-getunderlyingtype

Conversation

@AaronRobinsonMSFT

@AaronRobinsonMSFTAaronRobinsonMSFT commented Apr 14, 2026

Copy link
Copy Markdown
Member

Closes#125388
Fixes#124216

Breaking change documentation: dotnet/docs#53407

Summary

Adds a new public virtual Type.GetNullableUnderlyingType() method so that Type subclasses (e.g. MetadataLoadContext's RoType) can correctly identify Nullable types. Nullable.GetUnderlyingType() now forwards to this virtual.

This follows the same pattern as Enum.GetUnderlyingType() forwarding to Type.GetEnumUnderlyingType().

Contract

Changes

Public API

  • Type.cs: New public virtual Type? GetNullableUnderlyingType() that throws NotSupportedException(SR.NotSupported_SubclassOverride) (matches IsByRefLike pattern per @MichalStrehovsky's feedback). XML doc documents that the open generic Nullable<> is treated as nullable and yields the generic type parameter.
  • System.Runtime.cs / System.Reflection.Emit.cs (ref assemblies): New API + TypeDelegator/TypeBuilder/EnumBuilder/GenericTypeParameterBuilder overrides.

Nullable.GetUnderlyingType rewire

  • Nullable.cs: Now delegates to the new virtual after preserving the IsGenericTypeDefinition COMPAT short-circuit.

Runtime overrides (all three runtimes)

  • RuntimeType.CoreCLR.cs / RuntimeType.Mono.cs: Override that handles both constructed and open-generic cases. Open Nullable<> returns GetGenericArguments()[0] since the native fast-path can't yield a MethodTable for the formal type parameter T.
  • RuntimeType.NativeAot.cs: Same handling for constructed Nullable<X> via the EEType fast-path.
  • RuntimeTypeInfo.cs (NativeAOT): Added public virtual returning null (per @jkotas's feedback).
  • NativeFormatRuntimeNamedTypeInfo.cs (NativeAOT): Sealed override that returns the generic parameter only when the type is typeof(Nullable<>).
  • RuntimeConstructedGenericTypeInfo.cs (NativeAOT): override for constructed generics.

Reflection subclasses

  • TypeDelegator.cs: Override forwarding to typeImpl.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 returning null (or appropriate result for instantiations).
  • SymbolType.cs: Override returning null so Nullable.GetUnderlyingType doesn't throw on MakeArrayType/MakePointerType/MakeByRefType results from TypeBuilder.

MetadataLoadContext

  • RoType.cs: Override using CoreType.NullableT identity comparison; uses GetGenericArguments()[0] so the open Nullable<> returns the MLC-projected generic parameter rather than indexing empty GenericTypeArguments.
  • RoModifiedType.cs: Override delegating through the unmodified type (required because RoModifiedType.GetGenericTypeDefinition() throws).

Tests

  • NullableTests.cs: Coverage for RuntimeType (constructed + open-generic) and TypeDelegator.
  • SignatureTypes.cs: Coverage for SignatureConstructedGenericType and SignatureModifiedType.
  • ModifiedTypeTests.cs: New NullableModifiedTypeHolder (uses volatile delegate*<int?> to obtain a ModifiedType wrapping Nullable<int>) and tests for modified Nullable / non-Nullable.
  • TypeBuilderGetNullableUnderlyingType.cs (new): Coverage for TypeBuilder, EnumBuilder, GenericTypeParameterBuilder, TypeBuilderInstantiation, and SymbolType (Array / multi-dim Array / Pointer / ByRef).
  • TypeTests.Nullable.cs (MetadataLoadContext): Coverage for RoType (constructed + open-generic).

Note

This PR description was updated with assistance from GitHub Copilot.

AaronRobinsonMSFTand others added 3 commits March 9, 2026 23:30
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>
CopilotAI review requested due to automatic review settings April 14, 2026 20:57
@AaronRobinsonMSFTAaronRobinsonMSFT added this to the 11.0.0 milestone Apr 14, 2026
@AaronRobinsonMSFTAaronRobinsonMSFT changed the title Add Type.GetNullableUnderlyingType() virtual APIAdd Type.GetNullableUnderlyingType() virtual APIApr 14, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for RuntimeType (CoreCLR/Mono), NativeAOT RuntimeType, TypeDelegator, and MetadataLoadContext’s RoType.
  • Change Nullable.GetUnderlyingType(Type) to forward to Type.GetNullableUnderlyingType().
  • Add System.Runtime tests covering both RuntimeType and MetadataLoadContext behavior, plus a test project reference to System.Reflection.MetadataLoadContext.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() method.
src/libraries/System.Private.CoreLib/src/System/RuntimeType.csOverrides GetNullableUnderlyingType() for runtime types.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csAdds NativeAOT override of GetNullableUnderlyingType().
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual through TypeDelegator.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements nullable detection for MLC RoType.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to the new virtual.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates the ref assembly surface area for the new API and TypeDelegator override.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csSwitches Mono’s internal nullable check to use GetNullableUnderlyingType().
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() and MLC scenarios.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csprojAdds test-time project reference to System.Reflection.MetadataLoadContext.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 3

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Type.cs
- 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>
AaronRobinsonMSFTand others added 2 commits April 14, 2026 19:25
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 03:05

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wires Nullable.GetUnderlyingType() to call it.
  • Implements overrides for CoreCLR, Mono, NativeAOT RuntimeType, and key wrapper types (TypeDelegator, modified/signature types).
  • Adds tests for RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds new virtual GetNullableUnderlyingType() API.
src/libraries/System.Private.CoreLib/src/System/Nullable.csForwards Nullable.GetUnderlyingType to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csImplements CoreCLR RuntimeType override using MethodTable fast-path + fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csImplements Mono RuntimeType override; updates IsNullableOfT to use it.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csImplements NativeAOT RuntimeType override.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the underlying Type.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csExplicitly returns null for the new virtual.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csExplicitly returns null for the new virtual.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csImplements MLC RoType override via CoreType.NullableT identity.
src/libraries/System.Runtime/ref/System.Runtime.csAdds the new API to the ref assembly and TypeDelegator override surface.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds tests for Type.GetNullableUnderlyingType() on runtime types.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC-specific tests for nullable detection, including open-generic case.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes 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 GetNullableUnderlyingType test cases don't cover the open generic definition typeof(Nullable<>). Given the API contract is "closed generic Nullable only", add a case asserting typeof(Nullable<>).GetNullableUnderlyingType() returns null to 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

AaronRobinsonMSFTand others added 3 commits April 14, 2026 20:15
…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>
…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>
CopilotAI review requested due to automatic review settings April 15, 2026 04:49

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 wire Nullable.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 RuntimeType and MetadataLoadContext behavior.
Show a summary per file
FileDescription
src/libraries/System.Private.CoreLib/src/System/Type.csAdds the new public virtual GetNullableUnderlyingType() API and docs.
src/libraries/System.Private.CoreLib/src/System/Nullable.csUpdates Nullable.GetUnderlyingType to delegate to Type.GetNullableUnderlyingType().
src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.csCoreCLR override that recognizes Nullable<T> via method table fast-path and fallback.
src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.csMono override implementation + internal IsNullableOfT updated to use the new virtual.
src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.csNativeAOT override implementation.
src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.csForwards the new virtual to the delegated typeImpl.
src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.csForwards the new virtual to the underlying unmodified type.
src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.csSealed override returning null to preserve existing signature-type semantics.
src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.csAdds override to surface underlying T for constructed Nullable<T> in Reflection.Emit instantiations.
src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.csAdds RoType override (guarded) using core-type identity comparison for Nullable<T>.
src/libraries/System.Runtime/ref/System.Runtime.csUpdates ref surface area for Type and TypeDelegator.
src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.csAdds direct Type.GetNullableUnderlyingType() runtime tests.
src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.csAdds MLC tests validating both Nullable.GetUnderlyingType and the direct virtual call.
src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csprojIncludes the new MLC test file in the test project.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs Outdated
CopilotAI review requested due to automatic review settings April 27, 2026 16:40

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 1

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs Outdated
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>
@AaronRobinsonMSFTAaronRobinsonMSFT added the breaking-change Issue or PR that represents a breaking API or functional change over a previous release. label Apr 27, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 27, 2026
@dotnet-policy-service

dotnet-policy-serviceBot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET Breaking Change Notification DL.

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>
CopilotAI review requested due to automatic review settings April 28, 2026 00:35

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new

AaronRobinsonMSFTand others added 2 commits April 28, 2026 09:55
- 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>
CopilotAI review requested due to automatic review settings April 28, 2026 19:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 28/28 changed files
  • Comments generated: 2

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Nullable.cs
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

Copy link
Copy Markdown
MemberAuthor

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:

  • Email a link to the docs issue to the .NET Breaking Change Notification DL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet label Apr 28, 2026
@AaronRobinsonMSFT
AaronRobinsonMSFT merged commit 977f412 into dotnet:mainApr 29, 2026
153 of 160 checks passed
@AaronRobinsonMSFT
AaronRobinsonMSFT deleted the fix/124216-nullable-getunderlyingtype branch April 29, 2026 05:46
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 29, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Reflectionbreaking-changeIssue or PR that represents a breaking API or functional change over a previous release.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Type.GetNullableUnderlyingType() MetadataLoadContext: Nullable.GetUnderlyingType() always returns null

4 participants

@AaronRobinsonMSFT@jkotas@MichalStrehovsky