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 {