'How to use .NET reflection to check for nullable reference type

C# 8.0 introduces nullable reference types. Here's a simple class with a nullable property:

public class Foo
{
    public String? Bar { get; set; }
}

Is there a way to check a class property uses a nullable reference type via reflection?



Solution 1:[1]

In .NET 6, APIs were added to handle this, see this answer.

Prior to this, you need to read the attributes yourself. This appears to work, at least on the types I've tested it with.

public static bool IsNullable(PropertyInfo property) =>
    IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes);

public static bool IsNullable(FieldInfo field) =>
    IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes);

public static bool IsNullable(ParameterInfo parameter) =>
    IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes);

private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable<CustomAttributeData> customAttributes)
{
    if (memberType.IsValueType)
        return Nullable.GetUnderlyingType(memberType) != null;

    var nullable = customAttributes
        .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
    if (nullable != null && nullable.ConstructorArguments.Count == 1)
    {
        var attributeArgument = nullable.ConstructorArguments[0];
        if (attributeArgument.ArgumentType == typeof(byte[]))
        {
            var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArgument.Value!;
            if (args.Count > 0 && args[0].ArgumentType == typeof(byte))
            {
                return (byte)args[0].Value! == 2;
            }
        }
        else if (attributeArgument.ArgumentType == typeof(byte))
        {
            return (byte)attributeArgument.Value! == 2;
        }
    }

    for (var type = declaringType; type != null; type = type.DeclaringType)
    {
        var context = type.CustomAttributes
            .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
        if (context != null &&
            context.ConstructorArguments.Count == 1 &&
            context.ConstructorArguments[0].ArgumentType == typeof(byte))
        {
            return (byte)context.ConstructorArguments[0].Value! == 2;
        }
    }

    // Couldn't find a suitable attribute
    return false;
}

See this document for details.

The general gist is that either the property itself can have a [Nullable] attribute on it, or if it doesn't the enclosing type might have [NullableContext] attribute. We first look for [Nullable], then if we don't find it we look for [NullableContext] on the enclosing type.

The compiler might embed the attributes into the assembly, and since we might be looking at a type from a different assembly, we need to do a reflection-only load.

[Nullable] might be instantiated with an array, if the property is generic. In this case, the first element represents the actual property (and further elements represent generic arguments). [NullableContext] is always instantiated with a single byte.

A value of 2 means "nullable". 1 means "not nullable", and 0 means "oblivious".

Solution 2:[2]

.NET 6 Preview 7 adds reflection APIs to get nullability info.

Libraries: Reflection APIs for nullability information

Obviously, this only helps folks targeting .NET 6+.

Getting top-level nullability information

Imagine you’re implementing a serializer. Using these new APIs the serializer can check whether a given property can be set to null:

private NullabilityInfoContext _nullabilityContext = new NullabilityInfoContext();

private void DeserializePropertyValue(PropertyInfo p, object instance, object? value)
{
    if (value is null)
    {
        var nullabilityInfo = _nullabilityContext.Create(p);
        if (nullabilityInfo.WriteState is not NullabilityState.Nullable)
        {
            throw new MySerializerException($"Property '{p.GetType().Name}.{p.Name}'' cannot be set to null.");
        }
    }

    p.SetValue(instance, value);
}

Solution 3:[3]

I wrote a library to do reflection of NRT types - internally it looks at the generated attributes and gives you a simple API:

https://github.com/RicoSuter/Namotion.Reflection

Solution 4:[4]

A great answer there by @rico-suter !

The following is for those who just want a quick cut-and-paste solution until the real McCoy is available (see the proposal https://github.com/dotnet/runtime/issues/29723 ).

I put this together based on @canton7's post above plus a short look at the ideas in @rico-suter's code. The change from the @canton7's code is just abstracting the list of attribute sources and including a few new ones.

    private static bool IsAttributedAsNonNullable(this PropertyInfo propertyInfo)
    {
        return IsAttributedAsNonNullable(
            new dynamic?[] { propertyInfo },
            new dynamic?[] { propertyInfo.DeclaringType, propertyInfo.DeclaringType?.DeclaringType, propertyInfo.DeclaringType?.GetTypeInfo() }
        );
    }

    private static bool IsAttributedAsNonNullable(this ParameterInfo parameterInfo)
    {
        return IsAttributedAsNonNullable(
            new dynamic?[] { parameterInfo },
            new dynamic?[] { parameterInfo.Member, parameterInfo.Member.DeclaringType, parameterInfo.Member.DeclaringType?.DeclaringType, parameterInfo.Member.DeclaringType?.GetTypeInfo()
        );
    }

    private static bool IsAttributedAsNonNullable( dynamic?[] nullableAttributeSources, dynamic?[] nullableContextAttributeSources)
    {
        foreach (dynamic? nullableAttributeSource in nullableAttributeSources) {
            if (nullableAttributeSource == null) { continue; }
            CustomAttributeData? nullableAttribute = ((IEnumerable<CustomAttributeData>)nullableAttributeSource.CustomAttributes).FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
            if (nullableAttribute != null && nullableAttribute.ConstructorArguments.Count == 1) {
                CustomAttributeTypedArgument attributeArgument = nullableAttribute.ConstructorArguments[0];
                if (attributeArgument.ArgumentType == typeof(byte[])) {
                    var args = (ReadOnlyCollection<CustomAttributeTypedArgument>)(attributeArgument.Value ?? throw new NullReferenceException("Logic error!"));
                    if (args.Count > 0 && args[0].ArgumentType == typeof(byte)) {
                        byte value = (byte)(args[0].Value ?? throw new NullabilityLogicException());
                        return value == 1; // 0 = oblivious, 1 = nonnullable, 2 = nullable
                    }
                } else if (attributeArgument.ArgumentType == typeof(byte)) {
                    byte value = (byte)(attributeArgument.Value ?? throw new NullReferenceException("Logic error!"));
                    return value == 1;  // 0 = oblivious, 1 = nonnullable, 2 = nullable
                } else {
                    throw new InvalidOperationException($"Unrecognized argument type for NullableAttribute.");
                }
            }
        }
        foreach (dynamic? nullableContextAttributeSource in nullableContextAttributeSources) {
            if (nullableContextAttributeSource == null) { continue; }
            CustomAttributeData? nullableContextAttribute = ((IEnumerable<CustomAttributeData>)nullableContextAttributeSource.CustomAttributes).FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
            if (nullableContextAttribute != null && nullableContextAttribute.ConstructorArguments.Count == 1) {
                CustomAttributeTypedArgument attributeArgument = nullableContextAttribute.ConstructorArguments[0];
                if (attributeArgument.ArgumentType == typeof(byte)) {
                    byte value = (byte)(nullableContextAttribute.ConstructorArguments[0].Value ?? throw new NullabilityLogicException());
                    return value == 1;
                } else {
                    throw new InvalidOperationException($"Unrecognized argument type for NullableContextAttribute.");
                }
            }
        }
        return false;
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Willem
Solution 3 Rico Suter
Solution 4 sjb-sjb