0

For example, I might have a method with the following signature:

public async Task<ViewModel?> GetPersonUri()

Using reflection I would like to determine if the type parameter of Task is a nullable reference type. In the case, the answer would be yes. If it were:

public async Task<ViewModel> GetPersonUri()

the answer should be no.

How can I determine the nullability of a generic type parameter?

Dejan
  • 9,150
  • 8
  • 69
  • 117
  • 1
    See [this document](https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md) for details on how nullable metadata is stored – canton7 Mar 23 '22 at 09:50
  • I think that information is available with reflection. On the Type with `typeof(T)` or `GetType()` on the object you can get all Properties or just one. On the `PropertyInfo` object I think you can find a boolean `IsNullable` (not 100% shure). – Paul Sinnema Mar 23 '22 at 10:01
  • It's a bit different. Take a look here somewhere further down the page you'll find a solution. https://stackoverflow.com/questions/6026824/detecting-a-nullable-type-via-reflection – Paul Sinnema Mar 23 '22 at 10:08
  • @PaulSinnema I believe the SO answer you're referring to has nothing to do with C# 8.0 NRTs. – Dejan Mar 23 '22 at 10:16
  • @JeroenMostert yes it does! Thx!!! – Dejan Mar 23 '22 at 10:49
  • Note that you can close your own question as a dupe (without having to wait for others to vote). – Jeroen Mostert Mar 23 '22 at 10:50

1 Answers1

2

In .NET 6 onwards, we gained the NullabilityInfoContext type, see this answer for details.

Prior to this, you have to manually inspect the attributes yourself. This seems to do the trick:

public static bool GenericReturnTypeIsNullable(MethodInfo methodInfo)
{
    var nullableAttribute = methodInfo!.ReturnTypeCustomAttributes.GetCustomAttributes(true)
        .FirstOrDefault(x => x.GetType().FullName == "System.Runtime.CompilerServices.NullableAttribute");
    if (nullableAttribute != null)
    {
        var flagsField = nullableAttribute.GetType().GetField("NullableFlags");
        var flags = (byte[]?)flagsField?.GetValue(nullableAttribute);
        return flags != null && flags.Length >= 2 && flags[1] == 2;
    }
    
    if (CheckNullableContext(methodInfo.CustomAttributes))
        return true;
    
    for (var type = methodInfo.DeclaringType; type != null; type = type.DeclaringType)
    {
        if (CheckNullableContext(type.CustomAttributes))
            return true;
    }
    
    static bool CheckNullableContext(IEnumerable<CustomAttributeData> customAttributes)
    {
        var context = customAttributes.FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
        return context != null &&
            context.ConstructorArguments.Count == 1 &&
            context.ConstructorArguments[0].ArgumentType == typeof(byte) &&
            (byte)context.ConstructorArguments[0].Value! == 2;
    }

    return false;
}

See it on dotnetfiddle.net.

See this spec doc for details.

We end up needing to check a number of things.

If the method has a Nullable attribute on its return value, its NullableFlags field is a byte array, where each member of the array refers to successive generic types. A 0 means null-oblivious, 1 means not nullable, 2 means nullable. So in:

[return: System.Runtime.CompilerServices.Nullable(new byte[] { 1, 2 })]
public Task<string?> Foo() => ...

The 1 refers to the Task<T> itself, and indicates that it's not nullable. The 2 refers to the first generic type parameter, i.e. the string?, and indicates that it is nullable.

If this attribute doesn't exist, we need to check for a NullableContext attribute on the method itself. This provides a default nullability for everything which doesn't have a specified Nullable attribute. This takes a single byte, again 0 to 2.

If the method doesn't have this attribute, it might be on the containing type, or its containing type. Keep looking upwards until we find one (or not).

The slightly tricky bit is that the compiler emits the Nullable and NullableContext attributes directly into the assembly, which means that each assembly will have its own definition. So we need to use reflection to access them - we can't cast anything to a NullableAttribute or NullableContextAttribute.

Dejan
  • 9,150
  • 8
  • 69
  • 117
canton7
  • 37,633
  • 3
  • 64
  • 77
  • Thank you very much for this detailed answer. However, we can leverage the new .NET 6 reflection API as outlined in https://stackoverflow.com/questions/71545936/c-sharp-detect-nullable-reference-type-from-generic-argument-of-methods-return. Basically, I must admit that my question seems to be a duplicate of that one as Jeroen pointed out. – Dejan Mar 23 '22 at 10:48
  • Yep, see the first sentence of my answer – canton7 Mar 23 '22 at 10:55