From 90ed195999977991aecc567cc3e7a897a8d81ff3 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Mon, 9 Mar 2026 18:17:47 -0700 Subject: [PATCH 01/22] Add Type.GetNullableUnderlyingType() virtual API Add a new public virtual Type.GetNullableUnderlyingType() method that returns the underlying type T for Nullable, 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. Fixes dotnet#124216 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Nullable.cs | 11 +-- .../System.Private.CoreLib/src/System/Type.cs | 21 ++++++ .../Reflection/TypeLoading/Types/RoType.cs | 17 +++++ .../System.Runtime/ref/System.Runtime.cs | 1 + .../System.Runtime.Tests.csproj | 2 + .../System/NullableTests.cs | 69 +++++++++++++++++++ .../src/System/RuntimeType.Mono.cs | 2 +- 7 files changed, 112 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index 2eb6e5002968a2..a6701f15aafa1c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -104,16 +104,7 @@ public static bool Equals(T? n1, T? n2) where T : struct { ArgumentNullException.ThrowIfNull(nullableType); - if (nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) - { - // Instantiated generic type only - Type genericType = nullableType.GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { - return nullableType.GetGenericArguments()[0]; - } - } - return null; + return nullableType.GetNullableUnderlyingType(); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index 5a80ac4a18553a..c35e4149d4e700 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -602,6 +602,27 @@ protected virtual TypeCode GetTypeCodeImpl() public virtual bool IsInstanceOfType([NotNullWhen(true)] object? o) => o != null && IsAssignableFrom(o.GetType()); public virtual bool IsEquivalentTo([NotNullWhen(true)] Type? other) => this == other; + /// + /// Returns the underlying type argument of a type. + /// + /// + /// The type argument of the type if the current type represents + /// a closed generic ; otherwise, . + /// + public virtual Type? GetNullableUnderlyingType() + { + if (IsGenericType && !IsGenericTypeDefinition) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + { + return GetGenericArguments()[0]; + } + } + + return null; + } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "The single instance field on enum types is never trimmed")] [Intrinsic] diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index ced9b2e4c22efc..339bdff2ff3139 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -327,6 +327,23 @@ public sealed override Type MakeArrayType(int rank) private volatile RoType? _lazyUnderlyingEnumType; public sealed override Array GetEnumValues() => throw new InvalidOperationException(SR.Arg_InvalidOperation_Reflection); + // Nullable methods +#if NET + public sealed override Type? GetNullableUnderlyingType() + { + if (IsConstructedGenericType) + { + RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); + if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) + { + return GenericTypeArguments[0]; + } + } + + return null; + } +#endif + #if NET [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "Enum Types are not trimmed.")] diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 63b0c03545ff9a..2408c55ac31e63 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -6604,6 +6604,7 @@ protected Type() { } public virtual string? GetEnumName(object value) { throw null; } public virtual string[] GetEnumNames() { throw null; } public virtual System.Type GetEnumUnderlyingType() { throw null; } + public virtual System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("It might not be possible to create an array of the enum type at runtime. Use Enum.GetValues or the GetEnumValuesAsUnderlyingType method instead.")] public virtual System.Array GetEnumValues() { throw null; } public virtual System.Array GetEnumValuesAsUnderlyingType() { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index ce7a42a7f69699..ab693312b602ea 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -376,5 +376,7 @@ + + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index 4f3efc786e4623..dd342f4e71aff8 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Xunit; namespace System.Tests @@ -88,12 +91,78 @@ public static void GetUnderlyingType(Type nullableType, Type? expected) Assert.Equal(expected, Nullable.GetUnderlyingType(nullableType)); } + [Theory] + [InlineData(typeof(int?), typeof(int))] + [InlineData(typeof(int), null)] + [InlineData(typeof(G), null)] + public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected) + { + Assert.Equal(expected, type.GetNullableUnderlyingType()); + } + [Fact] public static void GetUnderlyingType_NullType_ThrowsArgumentNullException() { AssertExtensions.Throws("nullableType", () => Nullable.GetUnderlyingType((Type)null)); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] + public static void GetUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnderlyingType() + { + string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + var resolver = new PathAssemblyResolver(runtimeAssemblies); + using var mlc = new MetadataLoadContext(resolver); + + Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); + Type intType = coreAssembly.GetType("System.Int32")!; + Type nullableIntType = coreAssembly.GetType("System.Nullable`1")!.MakeGenericType(intType); + + // Test via Nullable.GetUnderlyingType (forwards to the virtual) + Type? underlying = Nullable.GetUnderlyingType(nullableIntType); + Assert.NotNull(underlying); + Assert.Equal("System.Int32", underlying.FullName); + + // Test via Type.GetNullableUnderlyingType directly + Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); + Assert.NotNull(underlyingDirect); + Assert.Equal("System.Int32", underlyingDirect.FullName); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] + public static void GetUnderlyingType_MetadataLoadContext_NonNullableTypes_ReturnsNull() + { + string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + var resolver = new PathAssemblyResolver(runtimeAssemblies); + using var mlc = new MetadataLoadContext(resolver); + + Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); + Type intType = coreAssembly.GetType("System.Int32")!; + Type stringType = coreAssembly.GetType("System.String")!; + Type kvpType = coreAssembly.GetType("System.Collections.Generic.KeyValuePair`2")!.MakeGenericType(intType, stringType); + + Assert.Null(Nullable.GetUnderlyingType(intType)); + Assert.Null(Nullable.GetUnderlyingType(stringType)); + Assert.Null(Nullable.GetUnderlyingType(kvpType)); + + Assert.Null(intType.GetNullableUnderlyingType()); + Assert.Null(stringType.GetNullableUnderlyingType()); + Assert.Null(kvpType.GetNullableUnderlyingType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] + public static void GetUnderlyingType_MetadataLoadContext_OpenNullable_ReturnsNull() + { + string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); + var resolver = new PathAssemblyResolver(runtimeAssemblies); + using var mlc = new MetadataLoadContext(resolver); + + Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); + Type openNullableType = coreAssembly.GetType("System.Nullable`1")!; + + Assert.Null(Nullable.GetUnderlyingType(openNullableType)); + Assert.Null(openNullableType.GetNullableUnderlyingType()); + } + [Fact] public static void GetValueRefOrDefaultRef_WithValue() { diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index f442f500bcd5b4..e656c90f04fa4e 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -2466,7 +2466,7 @@ public override string? FullName public sealed override bool HasSameMetadataDefinitionAs(MemberInfo other) => HasSameMetadataDefinitionAsCore(other); - internal bool IsNullableOfT => Nullable.GetUnderlyingType(this) != null; + internal bool IsNullableOfT => GetNullableUnderlyingType() is not null; public override bool IsSZArray { From 2e7a9fbbcba1fa3e03c956492d2a3056f1031f61 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 12:34:28 -0700 Subject: [PATCH 02/22] Address review feedback for Type.GetNullableUnderlyingType() - 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> --- .../src/System/RuntimeType.NativeAot.cs | 14 ++++++++++++++ .../src/System/Reflection/TypeDelegator.cs | 1 + .../src/System/RuntimeType.cs | 14 ++++++++++++++ .../System.Private.CoreLib/src/System/Type.cs | 14 +------------- src/libraries/System.Runtime/ref/System.Runtime.cs | 1 + 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index a0285f166c0eca..ed2266a751b129 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -122,6 +122,20 @@ public override Type GetEnumUnderlyingType() return Enum.InternalGetUnderlyingType(this); } + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType && !IsGenericTypeDefinition) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + { + return GetGenericArguments()[0]; + } + } + + return null; + } + public override bool IsEnumDefined(object value) { ArgumentNullException.ThrowIfNull(value); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs index d0ab8d07203f38..f657eaa93d3361 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/TypeDelegator.cs @@ -152,6 +152,7 @@ public TypeDelegator([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes. protected override bool IsValueTypeImpl() => typeImpl.IsValueType; protected override bool IsCOMObjectImpl() => typeImpl.IsCOMObject; public override bool IsByRefLike => typeImpl.IsByRefLike; + public override Type? GetNullableUnderlyingType() => typeImpl.GetNullableUnderlyingType(); public override bool IsConstructedGenericType => typeImpl.IsConstructedGenericType; public override bool IsCollectible => typeImpl.IsCollectible; diff --git a/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs b/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs index 26583ab2862c6b..ef1603e2149fdd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs @@ -184,6 +184,20 @@ public override Type GetEnumUnderlyingType() return Enum.InternalGetUnderlyingType(this); } + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType && !IsGenericTypeDefinition) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + { + return GetGenericArguments()[0]; + } + } + + return null; + } + public override int GetHashCode() => RuntimeHelpers.GetHashCode(this); internal RuntimeModule GetRuntimeModule() => RuntimeTypeHandle.GetModule(this); diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index c35e4149d4e700..03af71e8958bdb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -609,19 +609,7 @@ protected virtual TypeCode GetTypeCodeImpl() /// The type argument of the type if the current type represents /// a closed generic ; otherwise, . /// - public virtual Type? GetNullableUnderlyingType() - { - if (IsGenericType && !IsGenericTypeDefinition) - { - Type genericType = GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { - return GetGenericArguments()[0]; - } - } - - return null; - } + public virtual Type? GetNullableUnderlyingType() => throw new NotSupportedException(SR.NotSupported_SubclassOverride); [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2085:UnrecognizedReflectionPattern", Justification = "The single instance field on enum types is never trimmed")] diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 2408c55ac31e63..7ba8735bb49e71 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -13186,6 +13186,7 @@ public TypeDelegator([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers public override bool IsFunctionPointer { get { throw null; } } public override bool IsGenericMethodParameter { get { throw null; } } public override bool IsGenericTypeParameter { get { throw null; } } + public override System.Type? GetNullableUnderlyingType() { throw null; } public override bool IsSZArray { get { throw null; } } public override bool IsTypeDefinition { get { throw null; } } public override bool IsUnmanagedFunctionPointer { get { throw null; } } From 1aa46f54f24847b90edbebdc308df2bc3ea7d9d6 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 14:15:01 -0700 Subject: [PATCH 03/22] Improve MLC tests: throwOnError and stronger assertions - 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> --- .../System.Runtime.Tests/System/NullableTests.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index dd342f4e71aff8..b4094f54a67f00 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -114,18 +114,22 @@ public static void GetUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnde using var mlc = new MetadataLoadContext(resolver); Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type intType = coreAssembly.GetType("System.Int32")!; - Type nullableIntType = coreAssembly.GetType("System.Nullable`1")!.MakeGenericType(intType); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type nullableIntType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!.MakeGenericType(intType); // Test via Nullable.GetUnderlyingType (forwards to the virtual) Type? underlying = Nullable.GetUnderlyingType(nullableIntType); Assert.NotNull(underlying); Assert.Equal("System.Int32", underlying.FullName); + Assert.Same(intType, underlying); + Assert.NotSame(typeof(int), underlying); // Test via Type.GetNullableUnderlyingType directly Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); Assert.NotNull(underlyingDirect); Assert.Equal("System.Int32", underlyingDirect.FullName); + Assert.Same(intType, underlyingDirect); + Assert.NotSame(typeof(int), underlyingDirect); } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] @@ -136,9 +140,9 @@ public static void GetUnderlyingType_MetadataLoadContext_NonNullableTypes_Return using var mlc = new MetadataLoadContext(resolver); Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type intType = coreAssembly.GetType("System.Int32")!; - Type stringType = coreAssembly.GetType("System.String")!; - Type kvpType = coreAssembly.GetType("System.Collections.Generic.KeyValuePair`2")!.MakeGenericType(intType, stringType); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type stringType = coreAssembly.GetType("System.String", throwOnError: true)!; + Type kvpType = coreAssembly.GetType("System.Collections.Generic.KeyValuePair`2", throwOnError: true)!.MakeGenericType(intType, stringType); Assert.Null(Nullable.GetUnderlyingType(intType)); Assert.Null(Nullable.GetUnderlyingType(stringType)); @@ -157,7 +161,7 @@ public static void GetUnderlyingType_MetadataLoadContext_OpenNullable_ReturnsNul using var mlc = new MetadataLoadContext(resolver); Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type openNullableType = coreAssembly.GetType("System.Nullable`1")!; + Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; Assert.Null(Nullable.GetUnderlyingType(openNullableType)); Assert.Null(openNullableType.GetNullableUnderlyingType()); From e8a877d7241d89d7c33dcf68e45354681981c7c4 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 19:25:32 -0700 Subject: [PATCH 04/22] Efficient GetNullableUnderlyingType() via MethodTable for CoreCLR/NativeAOT/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> --- .../src/System/RuntimeType.CoreCLR.cs | 18 ++++++++++++++++++ .../src/System/RuntimeType.NativeAot.cs | 11 +++++------ .../src/System/Nullable.cs | 4 +++- .../src/System/RuntimeType.cs | 14 -------------- .../src/System/RuntimeType.Mono.cs | 14 ++++++++++++++ 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 9b8f2d0268c7a0..7f19192f1a0b9a 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3591,6 +3591,24 @@ public override GenericParameterAttributes GenericParameterAttributes #endregion #region Generics + + public override unsafe Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + TypeHandle th = GetNativeTypeHandle(); + if (!th.IsTypeDesc && th.AsMethodTable()->IsNullable) + { + RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)th.AsMethodTable()->InstantiationArg0()); + GC.KeepAlive(this); + return result; + } + if (typeof(Nullable<>) == GetGenericTypeDefinition()) + return GetGenericArguments()[0]; + } + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { return GetRootElementType().TypeHandle.GetInstantiationInternal(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index ed2266a751b129..95878c6a922f3f 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -124,15 +124,14 @@ public override Type GetEnumUnderlyingType() public override Type? GetNullableUnderlyingType() { - if (IsGenericType && !IsGenericTypeDefinition) + if (IsGenericType) { - Type genericType = GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { + MethodTable* pEEType = _pUnderlyingEEType; + if (pEEType != null && pEEType->NullableType != null) + return GetTypeFromMethodTable(pEEType->NullableType); + if (typeof(Nullable<>) == GetGenericTypeDefinition()) return GetGenericArguments()[0]; - } } - return null; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index a6701f15aafa1c..836bac03b4aa8a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -104,7 +104,9 @@ public static bool Equals(T? n1, T? n2) where T : struct { ArgumentNullException.ThrowIfNull(nullableType); - return nullableType.GetNullableUnderlyingType(); + if (nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) + return nullableType.GetNullableUnderlyingType(); + return null; } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs b/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs index ef1603e2149fdd..26583ab2862c6b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs @@ -184,20 +184,6 @@ public override Type GetEnumUnderlyingType() return Enum.InternalGetUnderlyingType(this); } - public override Type? GetNullableUnderlyingType() - { - if (IsGenericType && !IsGenericTypeDefinition) - { - Type genericType = GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(Nullable<>))) - { - return GetGenericArguments()[0]; - } - } - - return null; - } - public override int GetHashCode() => RuntimeHelpers.GetHashCode(this); internal RuntimeModule GetRuntimeModule() => RuntimeTypeHandle.GetModule(this); diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index e656c90f04fa4e..86f1df030de857 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -1396,6 +1396,20 @@ public override GenericParameterAttributes GenericParameterAttributes #region Generics + public override Type? GetNullableUnderlyingType() + { + if (IsGenericType) + { + Type genericType = GetGenericTypeDefinition(); + if (ReferenceEquals(genericType, typeof(Nullable<>))) + { + return GetGenericArguments()[0]; + } + } + + return null; + } + internal RuntimeType[] GetGenericArgumentsInternal() { RuntimeType[]? res = null; From 325f6990b953309240b75a20e1d5f6a7030b3747 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 20:02:18 -0700 Subject: [PATCH 05/22] Add GetNullableUnderlyingType() overrides for Emit/Modified types; move 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> --- .../src/System/RuntimeType.CoreCLR.cs | 12 ++-- .../Emit/TypeBuilderInstantiation.cs | 1 + .../src/System/Reflection/ModifiedType.cs | 1 + .../SignatureConstructedGenericType.cs | 1 + .../Reflection/SignatureModifiedType.cs | 1 + ...eflection.MetadataLoadContext.Tests.csproj | 1 + .../src/Tests/Type/TypeTests.Nullable.cs | 70 +++++++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 2 +- .../System.Runtime.Tests.csproj | 2 - .../System/NullableTests.cs | 63 ----------------- 10 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 7f19192f1a0b9a..80fcfb4c37edcc 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3597,11 +3597,15 @@ public override GenericParameterAttributes GenericParameterAttributes if (IsGenericType) { TypeHandle th = GetNativeTypeHandle(); - if (!th.IsTypeDesc && th.AsMethodTable()->IsNullable) + if (!th.IsTypeDesc) { - RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)th.AsMethodTable()->InstantiationArg0()); - GC.KeepAlive(this); - return result; + MethodTable* pMT = th.AsMethodTable(); + if (pMT->IsNullable) + { + RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); + GC.KeepAlive(this); + return result; + } } if (typeof(Nullable<>) == GetGenericTypeDefinition()) return GetGenericArguments()[0]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index 6bdbc966825e93..f51403d12e6a18 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,6 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } + public override Type? GetNullableUnderlyingType() => null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs index 89d5401a50b1bc..00068133250d36 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs @@ -78,6 +78,7 @@ public override Type[] GetOptionalCustomModifiers() public override bool ContainsGenericParameters => _unmodifiedType.ContainsGenericParameters; public override Type GetGenericTypeDefinition() => _unmodifiedType.GetGenericTypeDefinition(); public override bool IsGenericType => _unmodifiedType.IsGenericType; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType(); [DynamicallyAccessedMembers(InvokeMemberMembers)] public override object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index 152a4b6cef8867..e5833f96fe0eb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,6 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; + public sealed override Type? GetNullableUnderlyingType() => null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs index 40d5a9ab977b02..c61466732cf47c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureModifiedType.cs @@ -42,6 +42,7 @@ internal SignatureModifiedType(Type baseType, Type[] requiredCustomModifiers, Ty public override Type[] GenericTypeArguments => _unmodifiedType.GenericTypeArguments; public override int GenericParameterPosition => _unmodifiedType.GenericParameterPosition; internal override SignatureType? ElementType => HasElementType ? new SignatureModifiedType(_unmodifiedType.GetElementType()!, [], []) : null; + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType(); public override string Name => _unmodifiedType.Name; public override string? Namespace => _unmodifiedType.Namespace; public override bool IsEnum => _unmodifiedType.IsEnum; diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj index 95555906fc0cc6..0f670e4bfa1f8f 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/System.Reflection.MetadataLoadContext.Tests.csproj @@ -59,6 +59,7 @@ + diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs new file mode 100644 index 00000000000000..40b2f4463e1a19 --- /dev/null +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Xunit; + +namespace System.Reflection.Tests +{ + public static partial class TypeTests + { + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnderlyingType() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type nullableIntType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!.MakeGenericType(intType); + + Type? underlying = Nullable.GetUnderlyingType(nullableIntType); + Assert.NotNull(underlying); + Assert.Equal("System.Int32", underlying.FullName); + Assert.Same(intType, underlying); + Assert.NotSame(typeof(int), underlying); + + Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); + Assert.NotNull(underlyingDirect); + Assert.Equal("System.Int32", underlyingDirect.FullName); + Assert.Same(intType, underlyingDirect); + Assert.NotSame(typeof(int), underlyingDirect); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_NonNullableTypes_ReturnsNull() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; + Type stringType = coreAssembly.GetType("System.String", throwOnError: true)!; + + Assert.Null(Nullable.GetUnderlyingType(intType)); + Assert.Null(Nullable.GetUnderlyingType(stringType)); + + Assert.Null(intType.GetNullableUnderlyingType()); + Assert.Null(stringType.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable_ReturnsNull() + { + string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); + var resolver = new PathAssemblyResolver([coreAssemblyPath]); + using var mlc = new MetadataLoadContext(resolver, TestUtils.GetNameOfCoreAssembly()); + + Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); + Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; + + Assert.Null(Nullable.GetUnderlyingType(openNullableType)); + Assert.Null(openNullableType.GetNullableUnderlyingType()); + } + } +} + +#endif // NET diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index c73710517757b7..cc2a049fd0fcbe 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -13228,7 +13228,6 @@ public TypeDelegator([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers public override bool IsFunctionPointer { get { throw null; } } public override bool IsGenericMethodParameter { get { throw null; } } public override bool IsGenericTypeParameter { get { throw null; } } - public override System.Type? GetNullableUnderlyingType() { throw null; } public override bool IsSZArray { get { throw null; } } public override bool IsTypeDefinition { get { throw null; } } public override bool IsUnmanagedFunctionPointer { get { throw null; } } @@ -13279,6 +13278,7 @@ public TypeDelegator([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index 57dcb1ca53d893..429cf4f96fc88f 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -377,7 +377,5 @@ - - diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index b4094f54a67f00..3c4d34de05a490 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Xunit; @@ -106,67 +104,6 @@ public static void GetUnderlyingType_NullType_ThrowsArgumentNullException() AssertExtensions.Throws("nullableType", () => Nullable.GetUnderlyingType((Type)null)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] - public static void GetUnderlyingType_MetadataLoadContext_NullableInt_ReturnsUnderlyingType() - { - string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); - var resolver = new PathAssemblyResolver(runtimeAssemblies); - using var mlc = new MetadataLoadContext(resolver); - - Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; - Type nullableIntType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!.MakeGenericType(intType); - - // Test via Nullable.GetUnderlyingType (forwards to the virtual) - Type? underlying = Nullable.GetUnderlyingType(nullableIntType); - Assert.NotNull(underlying); - Assert.Equal("System.Int32", underlying.FullName); - Assert.Same(intType, underlying); - Assert.NotSame(typeof(int), underlying); - - // Test via Type.GetNullableUnderlyingType directly - Type? underlyingDirect = nullableIntType.GetNullableUnderlyingType(); - Assert.NotNull(underlyingDirect); - Assert.Equal("System.Int32", underlyingDirect.FullName); - Assert.Same(intType, underlyingDirect); - Assert.NotSame(typeof(int), underlyingDirect); - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] - public static void GetUnderlyingType_MetadataLoadContext_NonNullableTypes_ReturnsNull() - { - string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); - var resolver = new PathAssemblyResolver(runtimeAssemblies); - using var mlc = new MetadataLoadContext(resolver); - - Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type intType = coreAssembly.GetType("System.Int32", throwOnError: true)!; - Type stringType = coreAssembly.GetType("System.String", throwOnError: true)!; - Type kvpType = coreAssembly.GetType("System.Collections.Generic.KeyValuePair`2", throwOnError: true)!.MakeGenericType(intType, stringType); - - Assert.Null(Nullable.GetUnderlyingType(intType)); - Assert.Null(Nullable.GetUnderlyingType(stringType)); - Assert.Null(Nullable.GetUnderlyingType(kvpType)); - - Assert.Null(intType.GetNullableUnderlyingType()); - Assert.Null(stringType.GetNullableUnderlyingType()); - Assert.Null(kvpType.GetNullableUnderlyingType()); - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))] - public static void GetUnderlyingType_MetadataLoadContext_OpenNullable_ReturnsNull() - { - string[] runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"); - var resolver = new PathAssemblyResolver(runtimeAssemblies); - using var mlc = new MetadataLoadContext(resolver); - - Assembly coreAssembly = mlc.LoadFromAssemblyName("System.Runtime"); - Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; - - Assert.Null(Nullable.GetUnderlyingType(openNullableType)); - Assert.Null(openNullableType.GetNullableUnderlyingType()); - } - [Fact] public static void GetValueRefOrDefaultRef_WithValue() { From 5da1c09182206646022ce4e4143f4d77ea8329ff Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 20:15:28 -0700 Subject: [PATCH 06/22] Fix open-generic Nullable<> edge cases and preserve TypeBuilderInstantiation 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> --- .../System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs | 2 +- .../src/System/RuntimeType.NativeAot.cs | 2 +- .../src/System/Reflection/Emit/TypeBuilderInstantiation.cs | 2 +- .../tests/System.Runtime.Tests/System/NullableTests.cs | 1 + .../System.Private.CoreLib/src/System/RuntimeType.Mono.cs | 4 +--- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 80fcfb4c37edcc..1c4d7a7f2d6491 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3594,7 +3594,7 @@ public override GenericParameterAttributes GenericParameterAttributes public override unsafe Type? GetNullableUnderlyingType() { - if (IsGenericType) + if (IsConstructedGenericType) { TypeHandle th = GetNativeTypeHandle(); if (!th.IsTypeDesc) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index 95878c6a922f3f..91fc0c55a7fbe5 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -124,7 +124,7 @@ public override Type GetEnumUnderlyingType() public override Type? GetNullableUnderlyingType() { - if (IsGenericType) + if (IsConstructedGenericType) { MethodTable* pEEType = _pUnderlyingEEType; if (pEEType != null && pEEType->NullableType != null) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index f51403d12e6a18..6333a8a68f4fe8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,7 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } - public override Type? GetNullableUnderlyingType() => null; + public override Type? GetNullableUnderlyingType() => ReferenceEquals(_genericType, typeof(Nullable<>)) ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index 3c4d34de05a490..584fb41d3dcebc 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -93,6 +93,7 @@ public static void GetUnderlyingType(Type nullableType, Type? expected) [InlineData(typeof(int?), typeof(int))] [InlineData(typeof(int), null)] [InlineData(typeof(G), null)] + [InlineData(typeof(Nullable<>), null)] public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected) { Assert.Equal(expected, type.GetNullableUnderlyingType()); diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index 86f1df030de857..750dea84c9ffc5 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -1398,13 +1398,11 @@ public override GenericParameterAttributes GenericParameterAttributes public override Type? GetNullableUnderlyingType() { - if (IsGenericType) + if (IsConstructedGenericType) { Type genericType = GetGenericTypeDefinition(); if (ReferenceEquals(genericType, typeof(Nullable<>))) - { return GetGenericArguments()[0]; - } } return null; From 8a39a7ac67d2e73013f9f6b5c833849c457a128a Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 20:37:15 -0700 Subject: [PATCH 07/22] Remove unnecessary using directive for System.Runtime.InteropServices in NullableTests.cs --- .../tests/System.Runtime.Tests/System/NullableTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index 584fb41d3dcebc..d2dbf96ff9009b 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using Xunit; namespace System.Tests From 9f9b08534f7f457da10144b5475afba948087ee8 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 14 Apr 2026 21:49:28 -0700 Subject: [PATCH 08/22] Fix RoType.GetNullableUnderlyingType guard: use NET11_0_OR_GREATER not 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> --- .../src/System/Reflection/TypeLoading/Types/RoType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index 8634d8f4203803..1bfc9d67a5b458 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -328,7 +328,7 @@ public sealed override Type MakeArrayType(int rank) public sealed override Array GetEnumValues() => throw new InvalidOperationException(SR.Arg_InvalidOperation_Reflection); // Nullable methods -#if NET +#if NET11_0_OR_GREATER public sealed override Type? GetNullableUnderlyingType() { if (IsConstructedGenericType) From 4e99b77c3b5ba5125be10fa83c6872684601dd1d Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Wed, 15 Apr 2026 12:37:36 -0700 Subject: [PATCH 09/22] Fix SignatureConstructedGenericType.GetNullableUnderlyingType to return type arg for Nullable Previously returned null unconditionally. Now delegates to the generic type definition to check if it is Nullable<>, and if so returns the first type argument - consistent with jkotas's suggestion and with how TypeBuilderInstantiation handles this case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Reflection/SignatureConstructedGenericType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index e5833f96fe0eb8..67be6b60a67cb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,7 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; - public sealed override Type? GetNullableUnderlyingType() => null; + public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition.GetNullableUnderlyingType() is not null ? _genericTypeArguments[0] : null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); From 6a03178a3d6a635e4fb05ce89e4cba31a9e5574f Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Wed, 15 Apr 2026 12:46:35 -0700 Subject: [PATCH 10/22] Apply suggestions from code review Co-authored-by: Jan Kotas --- src/libraries/System.Private.CoreLib/src/System/Nullable.cs | 5 ++++- .../src/System/Reflection/Emit/TypeBuilderInstantiation.cs | 2 +- .../src/System/Reflection/TypeLoading/Types/RoType.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index 836bac03b4aa8a..f3a4d20681743c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -104,7 +104,10 @@ public static bool Equals(T? n1, T? n2) where T : struct { ArgumentNullException.ThrowIfNull(nullableType); - if (nullableType.IsGenericType && !nullableType.IsGenericTypeDefinition) + // COMPAT: Returns null for generic type definition + if (nullableType.IsGenericTypeDefinition) + return null; + return nullableType.GetNullableUnderlyingType(); return nullableType.GetNullableUnderlyingType(); return null; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index 6333a8a68f4fe8..b7167fde85e323 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,7 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } - public override Type? GetNullableUnderlyingType() => ReferenceEquals(_genericType, typeof(Nullable<>)) ? _typeArguments[0] : null; + public override Type? GetNullableUnderlyingType() => _genericType.GetNullableUnderlyingType() is not null ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index 1bfc9d67a5b458..4a48a7fa724df7 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -331,7 +331,7 @@ public sealed override Type MakeArrayType(int rank) #if NET11_0_OR_GREATER public sealed override Type? GetNullableUnderlyingType() { - if (IsConstructedGenericType) + if (IsGenericType) { RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) From 2153bf1fc683b3c8dbd5e9acddabf92ad127fc9c Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Wed, 15 Apr 2026 20:35:47 -0700 Subject: [PATCH 11/22] Apply suggestion from @jkotas --- src/libraries/System.Private.CoreLib/src/System/Nullable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index f3a4d20681743c..7f44a8a4765444 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -108,7 +108,6 @@ public static bool Equals(T? n1, T? n2) where T : struct if (nullableType.IsGenericTypeDefinition) return null; return nullableType.GetNullableUnderlyingType(); - return nullableType.GetNullableUnderlyingType(); return null; } From d7228abd4c2b3eb720c13a4613139cf83a573f5a Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Wed, 15 Apr 2026 20:42:45 -0700 Subject: [PATCH 12/22] Update src/libraries/System.Private.CoreLib/src/System/Nullable.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/libraries/System.Private.CoreLib/src/System/Nullable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs index 7f44a8a4765444..758af84f896c29 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Nullable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Nullable.cs @@ -107,8 +107,8 @@ public static bool Equals(T? n1, T? n2) where T : struct // COMPAT: Returns null for generic type definition if (nullableType.IsGenericTypeDefinition) return null; + return nullableType.GetNullableUnderlyingType(); - return null; } /// From 2a92fa3f436ebaf156dd6444dba6aa12d11e7e47 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Wed, 15 Apr 2026 23:33:21 -0700 Subject: [PATCH 13/22] Apply suggestion from @jkotas --- .../src/System/RuntimeType.CoreCLR.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 1c4d7a7f2d6491..c7f9debda103bd 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3594,21 +3594,16 @@ public override GenericParameterAttributes GenericParameterAttributes public override unsafe Type? GetNullableUnderlyingType() { - if (IsConstructedGenericType) + TypeHandle th = GetNativeTypeHandle(); + if (!th.IsTypeDesc) { - TypeHandle th = GetNativeTypeHandle(); - if (!th.IsTypeDesc) + MethodTable* pMT = th.AsMethodTable(); + if (pMT->IsNullable) { - MethodTable* pMT = th.AsMethodTable(); - if (pMT->IsNullable) - { - RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); - GC.KeepAlive(this); - return result; - } + RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); + GC.KeepAlive(this); + return result; } - if (typeof(Nullable<>) == GetGenericTypeDefinition()) - return GetGenericArguments()[0]; } return null; } From 8c08626b399870c4986ec2a21002ec3b873bfd77 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Thu, 16 Apr 2026 17:51:29 -0700 Subject: [PATCH 14/22] Fix GetNullableUnderlyingType crashes and wrong results InstantiationArg0() to prevent native assert crash on Nullable<> - NativeAOT: apply jkotas suggestion - check IsNullable before NullableType, guard IsGenericTypeDefinition, fall back to RuntimeTypeInfo.GetNullableUnderlyingType() - NativeAOT: add GetNullableUnderlyingType() to RuntimeTypeInfo and RuntimeConstructedGenericTypeInfo for the fallback path - SignatureConstructedGenericType: compare definition directly to typeof(Nullable<>) instead of calling virtual on definition (Thread 21) - TypeBuilderInstantiation: same fix as SignatureConstructedGenericType (Thread 22) - RoType: use IsConstructedGenericType instead of IsGenericType to prevent IndexOutOfRangeException on open Nullable<> (Thread 26) - Type.cs: update XML doc wording per reviewer suggestion (Thread 27) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/RuntimeType.CoreCLR.cs | 2 +- .../TypeInfos/RuntimeConstructedGenericTypeInfo.cs | 5 +++++ .../Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs | 2 ++ .../src/System/RuntimeType.NativeAot.cs | 12 ++++++------ .../Reflection/Emit/TypeBuilderInstantiation.cs | 2 +- .../Reflection/SignatureConstructedGenericType.cs | 2 +- .../System.Private.CoreLib/src/System/Type.cs | 2 +- .../System/Reflection/TypeLoading/Types/RoType.cs | 2 +- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index c7f9debda103bd..70174780ef7a5f 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3598,7 +3598,7 @@ public override GenericParameterAttributes GenericParameterAttributes if (!th.IsTypeDesc) { MethodTable* pMT = th.AsMethodTable(); - if (pMT->IsNullable) + if (pMT->IsNullable && !pMT->IsGenericTypeDefinition) { RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); GC.KeepAlive(this); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs index 4eaf089573fb1e..8794d70d9d223a 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeConstructedGenericTypeInfo.cs @@ -85,6 +85,11 @@ public sealed override Type GetGenericTypeDefinition() return GenericTypeDefinitionTypeInfo.ToType(); } + public sealed override Type? GetNullableUnderlyingType() => + GenericTypeDefinitionTypeInfo.ToType() == typeof(Nullable<>) + ? _key.GenericTypeArguments[0].ToType() + : null; + public sealed override Guid GUID { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs index 7d99042d8467e2..46b620c9cfc26b 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs @@ -399,6 +399,8 @@ public virtual Type GetGenericTypeDefinition() throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); } + public virtual Type? GetNullableUnderlyingType() => null; + public Type MakeArrayType() { // Do not implement this as a call to MakeArrayType(1) - they are not interchangeable. MakeArrayType() returns a diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index 91fc0c55a7fbe5..97a534a6d567d1 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -124,15 +124,15 @@ public override Type GetEnumUnderlyingType() public override Type? GetNullableUnderlyingType() { - if (IsConstructedGenericType) + MethodTable* pEEType = _pUnderlyingEEType; + if (pEEType != null) { - MethodTable* pEEType = _pUnderlyingEEType; - if (pEEType != null && pEEType->NullableType != null) + if (!pEEType->IsNullable) + return null; + if (!pEEType->IsGenericTypeDefinition) return GetTypeFromMethodTable(pEEType->NullableType); - if (typeof(Nullable<>) == GetGenericTypeDefinition()) - return GetGenericArguments()[0]; } - return null; + return GetRuntimeTypeInfo().GetNullableUnderlyingType(); } public override bool IsEnumDefined(object value) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index b7167fde85e323..0e82d690bb3703 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,7 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } - public override Type? GetNullableUnderlyingType() => _genericType.GetNullableUnderlyingType() is not null ? _typeArguments[0] : null; + public override Type? GetNullableUnderlyingType() => _genericType == typeof(Nullable<>) ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index 67be6b60a67cb8..a4fe016cb17dc9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,7 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; - public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition.GetNullableUnderlyingType() is not null ? _genericTypeArguments[0] : null; + public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition == typeof(Nullable<>) ? _genericTypeArguments[0] : null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index 03af71e8958bdb..3d9f0293f943f7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -607,7 +607,7 @@ protected virtual TypeCode GetTypeCodeImpl() /// /// /// The type argument of the type if the current type represents - /// a closed generic ; otherwise, . + /// a or its instantiation; otherwise, . /// public virtual Type? GetNullableUnderlyingType() => throw new NotSupportedException(SR.NotSupported_SubclassOverride); diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index 4a48a7fa724df7..1bfc9d67a5b458 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -331,7 +331,7 @@ public sealed override Type MakeArrayType(int rank) #if NET11_0_OR_GREATER public sealed override Type? GetNullableUnderlyingType() { - if (IsGenericType) + if (IsConstructedGenericType) { RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) From 2de90fccee1035de5a94b7b10f4396f3ebd31765 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Thu, 23 Apr 2026 18:12:17 -0700 Subject: [PATCH 15/22] Apply jkotas review feedback: add GetNullableUnderlyingType overrides - Add overrides on TypeBuilder, EnumBuilder, GenericTypeParameterBuilder so Reflection.Emit types don't throw from the base virtual. - Add safe null default on SignatureType base class for signature subclasses. - Fix TypeBuilderInstantiation, SignatureConstructedGenericType, ModifiedType to consult the underlying definition via GetNullableUnderlyingType. - Drop IsGenericTypeDefinition short-circuit in CoreCLR RuntimeType so the virtual returns T even for typeof(Nullable<>); static Nullable.GetUnderlyingType preserves COMPAT by filtering generic definitions. - Use IsGenericType (not IsConstructedGenericType) in Mono RuntimeType and MetadataLoadContext RoType for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs | 2 +- .../src/System/Reflection/Emit/EnumBuilder.cs | 3 +++ .../src/System/Reflection/Emit/GenericTypeParameterBuilder.cs | 3 +++ .../src/System/Reflection/Emit/TypeBuilder.cs | 3 +++ .../src/System/Reflection/Emit/TypeBuilderInstantiation.cs | 2 +- .../src/System/Reflection/ModifiedType.cs | 2 +- .../src/System/Reflection/SignatureConstructedGenericType.cs | 2 +- .../src/System/Reflection/SignatureType.cs | 4 ++++ .../System.Reflection.Emit/ref/System.Reflection.Emit.cs | 3 +++ .../src/System/Reflection/TypeLoading/Types/RoType.cs | 4 ++-- .../System.Private.CoreLib/src/System/RuntimeType.Mono.cs | 2 +- 11 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index 70174780ef7a5f..c7f9debda103bd 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3598,7 +3598,7 @@ public override GenericParameterAttributes GenericParameterAttributes if (!th.IsTypeDesc) { MethodTable* pMT = th.AsMethodTable(); - if (pMT->IsNullable && !pMT->IsGenericTypeDefinition) + if (pMT->IsNullable) { RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); GC.KeepAlive(this); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs index b8b250fb8507b3..d25baebf74282b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/EnumBuilder.cs @@ -11,6 +11,9 @@ protected EnumBuilder() { } + // An EnumBuilder represents an enum being built; it cannot itself be a Nullable. + public override Type? GetNullableUnderlyingType() => null; + public FieldBuilder UnderlyingField => UnderlyingFieldCore; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs index dfc38ba67ab088..a82f2a10dfdcfc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/GenericTypeParameterBuilder.cs @@ -11,6 +11,9 @@ protected GenericTypeParameterBuilder() { } + // A generic type parameter is not a Nullable instantiation. + public override Type? GetNullableUnderlyingType() => null; + public void SetCustomAttribute(ConstructorInfo con, byte[] binaryAttribute) => SetCustomAttributeCore(con, binaryAttribute); protected abstract void SetCustomAttributeCore(ConstructorInfo con, ReadOnlySpan binaryAttribute); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs index b1a0c16f804bd4..b5010936978822 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs @@ -12,6 +12,9 @@ protected TypeBuilder() { } + // A TypeBuilder represents a type being built; it cannot itself be a Nullable. + public override Type? GetNullableUnderlyingType() => null; + public const int UnspecifiedTypeSize = 0; public PackingSize PackingSize diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs index 0e82d690bb3703..b7167fde85e323 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilderInstantiation.cs @@ -249,7 +249,7 @@ public override bool ContainsGenericParameters } public override MethodBase? DeclaringMethod => null; public override Type GetGenericTypeDefinition() { return _genericType; } - public override Type? GetNullableUnderlyingType() => _genericType == typeof(Nullable<>) ? _typeArguments[0] : null; + public override Type? GetNullableUnderlyingType() => _genericType.GetNullableUnderlyingType() is not null ? _typeArguments[0] : null; [RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can't validate that the requirements of those annotations are met.")] public override Type MakeGenericType(params Type[] inst) { throw new InvalidOperationException(SR.Format(SR.Arg_NotGenericTypeDefinition, this)); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs index 00068133250d36..f4ed19e487a555 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/ModifiedType.cs @@ -78,7 +78,7 @@ public override Type[] GetOptionalCustomModifiers() public override bool ContainsGenericParameters => _unmodifiedType.ContainsGenericParameters; public override Type GetGenericTypeDefinition() => _unmodifiedType.GetGenericTypeDefinition(); public override bool IsGenericType => _unmodifiedType.IsGenericType; - public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType(); + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; [DynamicallyAccessedMembers(InvokeMemberMembers)] public override object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs index a4fe016cb17dc9..67be6b60a67cb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureConstructedGenericType.cs @@ -57,7 +57,7 @@ public sealed override bool ContainsGenericParameters internal sealed override SignatureType? ElementType => null; public sealed override int GetArrayRank() => throw new ArgumentException(SR.Argument_HasToBeArrayClass); public sealed override Type GetGenericTypeDefinition() => _genericTypeDefinition; - public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition == typeof(Nullable<>) ? _genericTypeArguments[0] : null; + public sealed override Type? GetNullableUnderlyingType() => _genericTypeDefinition.GetNullableUnderlyingType() is not null ? _genericTypeArguments[0] : null; public sealed override Type[] GetGenericArguments() => GenericTypeArguments; public sealed override Type[] GenericTypeArguments => (Type[])(_genericTypeArguments.Clone()); public sealed override int GenericParameterPosition => throw new InvalidOperationException(SR.Arg_NotGenericParameter); diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs index a29ac0441f48d7..ddf1ee78a6afae 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs @@ -18,6 +18,10 @@ internal abstract class SignatureType : Type { public sealed override bool IsSignatureType => true; + // SignatureType subclasses represent signature-only types (used for custom modifier signatures, etc.). + // Only SignatureConstructedGenericType can represent a Nullable; others override to no-op. + public override Type? GetNullableUnderlyingType() => null; + // Type flavor predicates public abstract override bool IsTypeDefinition { get; } protected abstract override bool HasElementTypeImpl(); diff --git a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs index bf5807a9cef73d..afef1b519c5ba1 100644 --- a/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs +++ b/src/libraries/System.Reflection.Emit/ref/System.Reflection.Emit.cs @@ -162,6 +162,7 @@ protected EnumBuilder() { } public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -293,6 +294,7 @@ protected GenericTypeParameterBuilder() { } public override System.Type GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] @@ -657,6 +659,7 @@ public void DefineMethodOverride(System.Reflection.MethodInfo methodInfoBody, Sy public override System.Type? GetNestedType(string name, System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes)] public override System.Type[] GetNestedTypes(System.Reflection.BindingFlags bindingAttr) { throw null; } + public override System.Type? GetNullableUnderlyingType() { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] public override System.Reflection.PropertyInfo[] GetProperties(System.Reflection.BindingFlags bindingAttr) { throw null; } [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index 1bfc9d67a5b458..70c13fd6e8dbb0 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -329,9 +329,9 @@ public sealed override Type MakeArrayType(int rank) // Nullable methods #if NET11_0_OR_GREATER - public sealed override Type? GetNullableUnderlyingType() + public override Type? GetNullableUnderlyingType() { - if (IsConstructedGenericType) + if (IsGenericType) { RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs index 750dea84c9ffc5..5ab601dceb9809 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeType.Mono.cs @@ -1398,7 +1398,7 @@ public override GenericParameterAttributes GenericParameterAttributes public override Type? GetNullableUnderlyingType() { - if (IsConstructedGenericType) + if (IsGenericType) { Type genericType = GetGenericTypeDefinition(); if (ReferenceEquals(genericType, typeof(Nullable<>))) From 92e3faffbf8527b6bdeab95d37198055eef1d843 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Fri, 24 Apr 2026 14:22:39 -0700 Subject: [PATCH 16/22] Treat Nullable<> generic type definition as nullable in Type.GetNullableUnderlyingType() Type.GetNullableUnderlyingType() now returns the generic type parameter T for typeof(Nullable<>), matching the new contract that the open generic type definition is considered nullable. Nullable.GetUnderlyingType(Type) continues to return null for generic type definitions for COMPAT. - Type.cs: updated XML doc. - RuntimeType.CoreCLR.cs / RuntimeType.NativeAot.cs: return GetGenericArguments()[0] for the open Nullable<> since the native fast paths can't yield a MethodTable for the formal type parameter T. - MetadataLoadContext RoType: switched from GenericTypeArguments[0] (empty for open generic) to GetGenericArguments()[0]. - Tests updated to expect non-null for typeof(Nullable<>) via Type.GetNullableUnderlyingType(), and still null via Nullable.GetUnderlyingType(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/RuntimeType.CoreCLR.cs | 7 +++++++ .../src/System/RuntimeType.NativeAot.cs | 2 ++ .../System.Private.CoreLib/src/System/Type.cs | 5 ++++- .../System/Reflection/TypeLoading/Types/RoType.cs | 5 ++++- .../tests/src/Tests/Type/TypeTests.Nullable.cs | 9 +++++++-- .../System.Runtime.Tests/System/NullableTests.cs | 14 ++++++++++---- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index c7f9debda103bd..b462ea38d1663d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3600,6 +3600,13 @@ public override GenericParameterAttributes GenericParameterAttributes MethodTable* pMT = th.AsMethodTable(); if (pMT->IsNullable) { + // The open generic Nullable<> is also classified as Nullable, + // but InstantiationArg0() does not yield a MethodTable for the type variable T. + // Fall back to the managed API which returns the generic type parameter. + if (pMT->IsGenericTypeDefinition) + { + return GetGenericArguments()[0]; + } RuntimeType result = RuntimeTypeHandle.GetRuntimeTypeFromHandle((IntPtr)pMT->InstantiationArg0()); GC.KeepAlive(this); return result; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index 97a534a6d567d1..aa4e2f2f2efeb7 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -131,6 +131,8 @@ public override Type GetEnumUnderlyingType() return null; if (!pEEType->IsGenericTypeDefinition) return GetTypeFromMethodTable(pEEType->NullableType); + // Open generic Nullable<>: return the generic type parameter T. + return GetGenericArguments()[0]; } return GetRuntimeTypeInfo().GetNullableUnderlyingType(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Type.cs b/src/libraries/System.Private.CoreLib/src/System/Type.cs index 3d9f0293f943f7..60bf2923b2b6f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Type.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Type.cs @@ -607,7 +607,10 @@ protected virtual TypeCode GetTypeCodeImpl() /// /// /// The type argument of the type if the current type represents - /// a or its instantiation; otherwise, . + /// the generic type definition or a constructed ; + /// otherwise, . When the current type is the generic type definition + /// (for example, typeof(Nullable<>)), the returned type is the generic type + /// parameter T. /// public virtual Type? GetNullableUnderlyingType() => throw new NotSupportedException(SR.NotSupported_SubclassOverride); diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs index 70c13fd6e8dbb0..3ee62bfd62d42c 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoType.cs @@ -336,7 +336,10 @@ public sealed override Type MakeArrayType(int rank) RoType? nullableOfT = Loader.TryGetCoreType(CoreType.NullableT); if (nullableOfT is not null && GetGenericTypeDefinition() == nullableOfT) { - return GenericTypeArguments[0]; + // Use GetGenericArguments() to cover both constructed Nullable + // (returns T) and the generic type definition Nullable<> + // (returns the generic type parameter). + return GetGenericArguments()[0]; } } diff --git a/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs index 40b2f4463e1a19..72b0bdf87f562a 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/tests/src/Tests/Type/TypeTests.Nullable.cs @@ -52,7 +52,7 @@ public static void GetNullableUnderlyingType_MetadataLoadContext_NonNullableType } [Fact] - public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable_ReturnsNull() + public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable() { string coreAssemblyPath = TestUtils.GetPathToCoreAssembly(); var resolver = new PathAssemblyResolver([coreAssemblyPath]); @@ -61,8 +61,13 @@ public static void GetNullableUnderlyingType_MetadataLoadContext_OpenNullable_Re Assembly coreAssembly = mlc.LoadFromAssemblyPath(coreAssemblyPath); Type openNullableType = coreAssembly.GetType("System.Nullable`1", throwOnError: true)!; + // Nullable.GetUnderlyingType returns null for generic type definitions (COMPAT). Assert.Null(Nullable.GetUnderlyingType(openNullableType)); - Assert.Null(openNullableType.GetNullableUnderlyingType()); + + // Type.GetNullableUnderlyingType returns the generic type parameter T for Nullable<>. + Type? underlying = openNullableType.GetNullableUnderlyingType(); + Assert.NotNull(underlying); + Assert.Same(openNullableType.GetGenericArguments()[0], underlying); } } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index d2dbf96ff9009b..e9146892efef7b 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -88,11 +88,17 @@ public static void GetUnderlyingType(Type nullableType, Type? expected) Assert.Equal(expected, Nullable.GetUnderlyingType(nullableType)); } + public static IEnumerable GetNullableUnderlyingType_RuntimeType_TestData() + { + yield return new object[] { typeof(int?), typeof(int) }; + yield return new object[] { typeof(int), null }; + yield return new object[] { typeof(G), null }; + // Nullable<> (generic type definition) is nullable; returns the generic type parameter T. + yield return new object[] { typeof(Nullable<>), typeof(Nullable<>).GetGenericArguments()[0] }; + } + [Theory] - [InlineData(typeof(int?), typeof(int))] - [InlineData(typeof(int), null)] - [InlineData(typeof(G), null)] - [InlineData(typeof(Nullable<>), null)] + [MemberData(nameof(GetNullableUnderlyingType_RuntimeType_TestData))] public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expected) { Assert.Equal(expected, type.GetNullableUnderlyingType()); From 85e3ec5f1b193a88ea47f7d21b2e951413c2a335 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Mon, 27 Apr 2026 09:40:15 -0700 Subject: [PATCH 17/22] Address review feedback: fix Nullable<> over foreign generic parameter - Make RuntimeTypeInfo.GetNullableUnderlyingType non-virtual with a proper implementation covering closed Nullable, the open Nullable<> definition, and Nullable where T is a generic parameter of another type (the case from jkotas's repro). - Drop the open-generic special case in RuntimeType.NativeAot.cs and fall through to RuntimeTypeInfo.GetNullableUnderlyingType. - Remove obsolete comment on TypeBuilder.GetNullableUnderlyingType. - Add NullableTests coverage for the foreign-generic-parameter case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs | 9 ++++++++- .../src/System/RuntimeType.NativeAot.cs | 2 -- .../src/System/Reflection/Emit/TypeBuilder.cs | 1 - .../System.Runtime.Tests/System/NullableTests.cs | 13 +++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs index 46b620c9cfc26b..99ff083bdd7535 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs @@ -399,7 +399,14 @@ public virtual Type GetGenericTypeDefinition() throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); } - public virtual Type? GetNullableUnderlyingType() => null; + public virtual Type? GetNullableUnderlyingType() + { + if (!IsGenericType) + return null; + if (GetGenericTypeDefinition() != typeof(Nullable<>)) + return null; + return IsGenericTypeDefinition ? GenericTypeParameters[0] : GenericTypeArguments[0]; + } public Type MakeArrayType() { diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs index aa4e2f2f2efeb7..97a534a6d567d1 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/RuntimeType.NativeAot.cs @@ -131,8 +131,6 @@ public override Type GetEnumUnderlyingType() return null; if (!pEEType->IsGenericTypeDefinition) return GetTypeFromMethodTable(pEEType->NullableType); - // Open generic Nullable<>: return the generic type parameter T. - return GetGenericArguments()[0]; } return GetRuntimeTypeInfo().GetNullableUnderlyingType(); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs index b5010936978822..a782db50ffbcba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/TypeBuilder.cs @@ -12,7 +12,6 @@ protected TypeBuilder() { } - // A TypeBuilder represents a type being built; it cannot itself be a Nullable. public override Type? GetNullableUnderlyingType() => null; public const int UnspecifiedTypeSize = 0; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs index e9146892efef7b..94cebad747f2f4 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/NullableTests.cs @@ -104,6 +104,17 @@ public static void GetNullableUnderlyingType_RuntimeType(Type type, Type? expect Assert.Equal(expected, type.GetNullableUnderlyingType()); } + [Fact] + public static void GetNullableUnderlyingType_NullableOverForeignGenericParameter() + { + // Nullable instantiated over the generic parameter of another type. + Type genericParam = typeof(GStruct<>).GetGenericArguments()[0]; + Type nullableOverParam = typeof(Nullable<>).MakeGenericType(genericParam); + + Assert.Same(genericParam, nullableOverParam.GetNullableUnderlyingType()); + Assert.Same(genericParam, Nullable.GetUnderlyingType(nullableOverParam)); + } + [Fact] public static void GetUnderlyingType_NullType_ThrowsArgumentNullException() { @@ -238,5 +249,7 @@ private struct MutatingStruct } public class G { } + + public struct GStruct where T : struct { } } } From 475eb35a798b11bda210ea4ce25fe37afc2fa414 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Mon, 27 Apr 2026 11:38:59 -0700 Subject: [PATCH 18/22] Fix misleading comment on SignatureType.GetNullableUnderlyingType SignatureModifiedType also overrides this method to surface the unmodified type's Nullable behavior, so the previous comment was inaccurate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Reflection/SignatureType.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs index ddf1ee78a6afae..8995f99c2dbb86 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/SignatureType.cs @@ -18,8 +18,7 @@ internal abstract class SignatureType : Type { public sealed override bool IsSignatureType => true; - // SignatureType subclasses represent signature-only types (used for custom modifier signatures, etc.). - // Only SignatureConstructedGenericType can represent a Nullable; others override to no-op. + // The base implementation does not expose Nullable behavior; subclasses override when appropriate. public override Type? GetNullableUnderlyingType() => null; // Type flavor predicates From 0e694d9a60582eccc5481089c40dbbed2c05b43d Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Mon, 27 Apr 2026 17:35:34 -0700 Subject: [PATCH 19/22] Fix CoreCLR assert when Nullable is instantiated over a generic variable When Nullable 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 contains generic variables (covers both the open Nullable<> definition and Nullable over a generic parameter). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/RuntimeType.CoreCLR.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index b462ea38d1663d..088587f84ed69d 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -3600,10 +3600,11 @@ public override GenericParameterAttributes GenericParameterAttributes MethodTable* pMT = th.AsMethodTable(); if (pMT->IsNullable) { - // The open generic Nullable<> is also classified as Nullable, - // but InstantiationArg0() does not yield a MethodTable for the type variable T. - // Fall back to the managed API which returns the generic type parameter. - if (pMT->IsGenericTypeDefinition) + // The open generic Nullable<> is also classified as Nullable, and a constructed + // Nullable instantiated over a generic variable holds a TypeDesc (not a + // MethodTable*) in InstantiationArg0(). Fall back to managed reflection in + // those cases. + if (pMT->ContainsGenericVariables) { return GetGenericArguments()[0]; } From bc3b14dfc82c354898f9abd9b8b5c39d25639733 Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 28 Apr 2026 09:55:44 -0700 Subject: [PATCH 20/22] Address PR feedback: refactor NativeAOT override and add tests - 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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NativeFormatRuntimeNamedTypeInfo.cs | 5 +++ .../Runtime/TypeInfos/RuntimeTypeInfo.cs | 9 +--- .../Common/tests/System/ModifiedTypeTests.cs | 31 +++++++++++++ .../tests/System.Reflection.Emit.Tests.csproj | 1 + .../TypeBuilderGetNullableUnderlyingType.cs | 44 +++++++++++++++++++ .../System/Reflection/SignatureTypes.cs | 32 ++++++++++++++ 6 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs index 2a0fc6f176cfbf..7284481f7bc4f9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs @@ -199,6 +199,11 @@ public sealed override string Name protected sealed override IEnumerable TrueCustomAttributes => RuntimeCustomAttributeData.GetCustomAttributes(_reader, _typeDefinition.CustomAttributes); + public sealed override Type? GetNullableUnderlyingType() + { + return (this.ToType() == typeof(Nullable<>)) ? RuntimeGenericTypeParameters[0].ToType() : null; + } + internal sealed override RuntimeTypeInfo[] RuntimeGenericTypeParameters { get diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs index 99ff083bdd7535..46b620c9cfc26b 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/RuntimeTypeInfo.cs @@ -399,14 +399,7 @@ public virtual Type GetGenericTypeDefinition() throw new InvalidOperationException(SR.InvalidOperation_NotGenericType); } - public virtual Type? GetNullableUnderlyingType() - { - if (!IsGenericType) - return null; - if (GetGenericTypeDefinition() != typeof(Nullable<>)) - return null; - return IsGenericTypeDefinition ? GenericTypeParameters[0] : GenericTypeArguments[0]; - } + public virtual Type? GetNullableUnderlyingType() => null; public Type MakeArrayType() { diff --git a/src/libraries/Common/tests/System/ModifiedTypeTests.cs b/src/libraries/Common/tests/System/ModifiedTypeTests.cs index 0175ddc892fa7e..2e13c3b8242f60 100644 --- a/src/libraries/Common/tests/System/ModifiedTypeTests.cs +++ b/src/libraries/Common/tests/System/ModifiedTypeTests.cs @@ -867,5 +867,36 @@ public static delegate* delegate* // ret > _fcnPtr_complex; } + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType() + { + FieldInfo fi = typeof(NullableModifiedTypeHolder).Project().GetField(nameof(NullableModifiedTypeHolder._fcnPtr_NullableReturn), Bindings); + + // The function pointer's return type is Nullable. Pulling the modified return type + // produces a ModifiedType wrapping Nullable, which exercises the override. + Type modifiedNullable = fi.GetModifiedFieldType().GetFunctionPointerReturnType(); + Assert.True(IsModifiedType(modifiedNullable)); + Assert.Equal(typeof(int?).Project(), modifiedNullable.UnderlyingSystemType); + + Type modifiedUnderlying = modifiedNullable.GetNullableUnderlyingType(); + Assert.NotNull(modifiedUnderlying); + Assert.True(IsModifiedType(modifiedUnderlying)); + Assert.Equal(typeof(int).Project(), modifiedUnderlying.UnderlyingSystemType); + Assert.Same(modifiedNullable.GetGenericArguments()[0], modifiedUnderlying); + } + + [Fact] + public static unsafe void GetNullableUnderlyingType_ModifiedType_NonNullable_ReturnsNull() + { + FieldInfo fi = typeof(ModifiedTypeHolder).Project().GetField(nameof(ModifiedTypeHolder._volatileInt), Bindings); + Type modifiedInt = fi.GetModifiedFieldType(); + Assert.True(IsModifiedType(modifiedInt)); + Assert.Null(modifiedInt.GetNullableUnderlyingType()); + } + + public unsafe class NullableModifiedTypeHolder + { + public static volatile delegate* _fcnPtr_NullableReturn; + } } } diff --git a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj index 64f0b179faaebb..f944d2e9e00cc2 100644 --- a/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj +++ b/src/libraries/System.Reflection.Emit/tests/System.Reflection.Emit.Tests.csproj @@ -117,6 +117,7 @@ + diff --git a/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs new file mode 100644 index 00000000000000..304f1a2bd2e077 --- /dev/null +++ b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Reflection.Emit.Tests +{ + public class TypeBuilderGetNullableUnderlyingType + { + [Fact] + public void TypeBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Assert.Null(tb.GetNullableUnderlyingType()); + } + + [Fact] + public void EnumBuilder_ReturnsNull() + { + EnumBuilder eb = Helpers.DynamicEnum(TypeAttributes.Public, typeof(int)); + Assert.Null(eb.GetNullableUnderlyingType()); + } + + [Fact] + public void GenericTypeParameterBuilder_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + GenericTypeParameterBuilder[] gps = tb.DefineGenericParameters("T"); + Assert.Null(gps[0].GetNullableUnderlyingType()); + } + + [Fact] + public void TypeBuilderInstantiation_ReturnsNull() + { + // A TypeBuilderInstantiation is produced when MakeGenericType is called on a + // generic TypeBuilder. The open generic definition is a TypeBuilder (never + // typeof(Nullable<>)), so the override always returns null in practice. + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + tb.DefineGenericParameters("T"); + Type instantiation = tb.MakeGenericType(typeof(int)); + Assert.Null(instantiation.GetNullableUnderlyingType()); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs index 870982a7949d05..ba8f8d5584eef7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Reflection/SignatureTypes.cs @@ -934,5 +934,37 @@ private static void TestSignatureTypeInvariants(Type type) Assert.Throws(() => type.GenericParameterPosition); } } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_Nullable_ReturnsTypeArgument() + { + Type sig = Type.MakeGenericSignatureType(typeof(Nullable<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureConstructedGenericType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeGenericSignatureType(typeof(List<>), typeof(int)); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_Nullable_DelegatesToUnmodifiedType() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int?), null, null); + Assert.True(sig.IsSignatureType); + Assert.Equal(typeof(int), sig.GetNullableUnderlyingType()); + } + + [Fact] + public static void GetNullableUnderlyingType_SignatureModifiedType_NonNullable_ReturnsNull() + { + Type sig = Type.MakeModifiedSignatureType(typeof(int), null, null); + Assert.True(sig.IsSignatureType); + Assert.Null(sig.GetNullableUnderlyingType()); + } } } From f631afaab200af2801e3c6b5452b331f494589bd Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 28 Apr 2026 12:11:21 -0700 Subject: [PATCH 21/22] Override GetNullableUnderlyingType on RoModifiedType The MetadataLoadContext RoModifiedType.GetGenericTypeDefinition() throws NotSupportedException, which caused the base RoType.GetNullableUnderlyingType to fail on modified Nullable instances. Mirror the runtime ModifiedType override so the modified generic argument is returned instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Reflection/TypeLoading/Types/RoModifiedType.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs index 1ec881fd27a9a3..0cf1eaf4f7f2ef 100644 --- a/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs +++ b/src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/TypeLoading/Types/RoModifiedType.cs @@ -168,6 +168,10 @@ public override IEnumerable DeclaredNestedTypes public override Type GetGenericTypeDefinition() => throw new NotSupportedException(SR.NotSupported_ModifiedType); +#if NET11_0_OR_GREATER + public override Type? GetNullableUnderlyingType() => _unmodifiedType.GetNullableUnderlyingType() is not null ? GetGenericArguments()[0] : null; +#endif + // Generic parameters are supported. internal override RoType[] GetGenericTypeParametersNoCopy() => _unmodifiedType.GetGenericTypeParametersNoCopy(); internal override RoType[] GetGenericTypeArgumentsNoCopy() => _unmodifiedType.GetGenericTypeArgumentsNoCopy(); From 1c5764b9054aaf99ad60cffaa0f20ede2690489f Mon Sep 17 00:00:00 2001 From: Aaron R Robinson Date: Tue, 28 Apr 2026 13:20:53 -0700 Subject: [PATCH 22/22] Override GetNullableUnderlyingType on SymbolType 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> --- .../src/System/Reflection/Emit/SymbolType.cs | 2 ++ .../TypeBuilderGetNullableUnderlyingType.cs | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs index a17e82b464c725..b154dac0d48b1d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/SymbolType.cs @@ -261,6 +261,8 @@ internal void SetFormat(string format, int curIndex, int length) public override bool IsSZArray => _rank <= 1 && _isSzArray; + public override Type? GetNullableUnderlyingType() => null; + public override Type MakePointerType() { return FormCompoundType(_format + "*", _baseType, 0)!; diff --git a/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs index 304f1a2bd2e077..bed425a32e1ce9 100644 --- a/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs +++ b/src/libraries/System.Reflection.Emit/tests/TypeBuilder/TypeBuilderGetNullableUnderlyingType.cs @@ -40,5 +40,41 @@ public void TypeBuilderInstantiation_ReturnsNull() Type instantiation = tb.MakeGenericType(typeof(int)); Assert.Null(instantiation.GetNullableUnderlyingType()); } + + [Fact] + public void SymbolType_Array_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_MultiDimArray_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type arrayType = tb.MakeArrayType(2); + Assert.Null(arrayType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(arrayType)); + } + + [Fact] + public void SymbolType_Pointer_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type pointerType = tb.MakePointerType(); + Assert.Null(pointerType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(pointerType)); + } + + [Fact] + public void SymbolType_ByRef_ReturnsNull() + { + TypeBuilder tb = Helpers.DynamicType(TypeAttributes.Public); + Type byRefType = tb.MakeByRefType(); + Assert.Null(byRefType.GetNullableUnderlyingType()); + Assert.Null(Nullable.GetUnderlyingType(byRefType)); + } } }