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

Viewed 5905

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?

5 Answers

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".

.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);
}

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;
    }

Late answer.

This is what I ended up using thanks to Bill Menees:

static bool IsMarkedAsNullable(PropertyInfo p)
{
    return new NullabilityInfoContext().Create(p).WriteState is NullabilityState.Nullable;
}

// Tests:


class Foo
{
        public int Int1 { get; set; }
        public int? Int2 { get; set; } = null;


        public string Str1 { get; set; } = "";
        public string? Str2 { get; set; } = null;

        
        public List<Foo> Lst1 { get; set; } = new();
        public List<Foo>? Lst2 { get; set; } = null;


        public Dictionary<int, object> Dic1 { get; set; } = new();
        public Dictionary<int, object>? Dic2 { get; set; } = null;
}

....

var props = typeof(Foo).GetProperties();
foreach(var prop in props)
{
    Console.WriteLine($"Prop:{prop.Name} IsNullable:{IsMarkedAsNullable(prop)}");
}


// outputs:

Prop:Int1 IsNullable:False
Prop:Int2 IsNullable:True
Prop:Str1 IsNullable:False
Prop:Str2 IsNullable:True
Prop:Lst1 IsNullable:False
Prop:Lst2 IsNullable:True
Prop:Dic1 IsNullable:False
Prop:Dic2 IsNullable:True

Related