Skip to content

NullabilityInfoContext returns incorrect NullabilityInfo values for Generics #115014

@ShawnOwczarzak

Description

@ShawnOwczarzak

Description

When using the NullabilityInfoContext to fetch NullabilityInfo for a generic property, the ReadState and WriteState are always Nullable, even when the defined generic has the type as NotNull.

Reproduction Steps

using System.Reflection;

#nullable enable
namespace NullabilityInfoWithGenerics
{
    internal class Program
    {
        static void Main(string[] args)
        {
            PropertyInfo valueProperty = typeof(MyGenericClass<string>).GetProperty("Value")!;
            NullabilityInfo NullabilityInfo = new NullabilityInfoContext().Create(valueProperty);

            // Expect NotNull
            Console.WriteLine(NullabilityInfo.ReadState);
        }
    }
    public record MyGenericClass<T>(T Value);   
}
#nullable restore

Expected behavior

The resulting NullabilityInfo's ReadState and WriteState should match the implemented generic type. string == NotNull. string? == Nullable

Actual behavior

When genaric T is a reference type, NullablityInfo has the ReadState Nullable.

Regression?

No response

Known Workarounds

Not using generics works fine (see below). But it's not really a work around.

Configuration

Dotnet 8.0
Nullable enabled

Other information

More Expected/Actual

Type                             | Expected   | Actual
-------------------------------------------------------
MyGenericClass`1<String>         | NotNull    | Nullable
MyGenericClass`1<String>         | Nullable   | Nullable
MyGenericClass`1<Int32>          | NotNull    | NotNull
MyGenericClass`1<Int32?>         | Nullable   | Nullable
MyGenericClass`1<TestClass>      | NotNull    | Nullable
MyGenericClass`1<TestClass?>     | Nullable   | Nullable
MyStringClass                    | NotNull    | NotNull
MyNullableStringClass            | Nullable   | Nullable
MyIntClass                       | NotNull    | NotNull
MyNullableIntClass               | Nullable   | Nullable
MyTestClassClass                 | NotNull    | NotNull
MyNullableTestClassClass         | Nullable   | Nullable
using System.Reflection;

#nullable enable
namespace NullabilityInfoWithGenerics
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("{0, -30} | {1, -10} | {2} ", "Type", "Expected", "Actual");
            Console.WriteLine("-------------------------------------------------------");

            CheckNullability<MyGenericClass<string>>(NullabilityState.NotNull);  // Mismatch
            CheckNullability<MyGenericClass<string?>>(NullabilityState.Nullable);
            
            CheckNullability<MyGenericClass<int>>(NullabilityState.NotNull);
            CheckNullability<MyGenericClass<int?>>(NullabilityState.Nullable);
            
            CheckNullability<MyGenericClass<TestClass?>>(NullabilityState.NotNull);  // Mismatch
            CheckNullability<MyGenericClass<TestClass?>>(NullabilityState.Nullable);

            CheckNullability<MyStringClass>(NullabilityState.NotNull);
            CheckNullability<MyNullableStringClass>(NullabilityState.Nullable);

            CheckNullability<MyIntClass>(NullabilityState.NotNull);
            CheckNullability<MyNullableIntClass>(NullabilityState.Nullable);

            CheckNullability<MyTestClassClass>(NullabilityState.NotNull);
            CheckNullability<MyNullableTestClassClass>(NullabilityState.Nullable);
        }

        static void CheckNullability<T>(NullabilityState expected)
        {
            PropertyInfo valueProperty = typeof(T).GetProperty("Value")!;
            NullabilityInfo NullabilityInfo = new NullabilityInfoContext().Create(valueProperty);

            if(expected != NullabilityInfo.ReadState)
            {
                Console.BackgroundColor = ConsoleColor.Yellow;
                Console.ForegroundColor = ConsoleColor.Black;
            }

            Console.WriteLine(
                "{0, -30} | {1, -10} | {2} ", 
                string.Format("{0}<{1}>", typeof(T).Name, string.Join(',', typeof(T).GetGenericArguments().Select(x => x.Name))), 
                expected, 
                NullabilityInfo.ReadState);

            if (expected != NullabilityInfo.ReadState)
                Console.ResetColor();
        }
    }
    public record MyGenericClass<T>(T Value);    
    public record MyStringClass(string Value);
    public record MyNullableStringClass(string? Value);
    public record MyIntClass(int Value);
    public record MyNullableIntClass(int? Value);
    public record MyTestClassClass(TestClass Value);
    public record MyNullableTestClassClass(TestClass? Value);
    public record TestClass { }
}
#nullable restore

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.ReflectionquestionAnswer questions and provide assistance, not an issue with source code or documentation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions