73

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?

canton7
  • 37,633
  • 3
  • 64
  • 77
shadeglare
  • 7,006
  • 7
  • 47
  • 59
  • compiling and looking at the IL, it looks like this adds `[NullableContext(2), Nullable((byte) 0)]` to the **type** (`Foo`) - so that's what to check for, but I'd need to dig more to understand the rules of how to interpret that! – Marc Gravell Oct 18 '19 at 15:34
  • 6
    Yes, but it's not trivial. Fortunately, it *is* [documented](https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-metadata.md). – Jeroen Mostert Oct 18 '19 at 15:36
  • ah, I see; so `string? X` gets no attributes, and `string Y` gets `[Nullable((byte)2)]` with `[NullableContext(2)]` on the accessors – Marc Gravell Oct 18 '19 at 15:37
  • 3
    If a type *just* contains nullables (or non-nullables), then that's all represented by `NullableContext`. If there's a mix, then `Nullable` used as well. `NullableContext` is an optimization to try and avoid having to emit `Nullable` all over the place. – canton7 Oct 18 '19 at 15:39

6 Answers6

55

In .NET 6, the NullabilityInfoContext 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".

canton7
  • 37,633
  • 3
  • 64
  • 77
  • It's really tricky. I just found a use case that is not covered by this code. public interface `IBusinessRelation : ICommon {}` / `public interface ICommon { string? Name {get;set;} }`. If I call the method on `IBusinessRelation` with the Property `Name` I get false. – gsharp Oct 29 '19 at 08:07
  • @gsharp Ah, I hadn't tried it with interfaces, or any sort of inheritance. I'm guessing it's a relatively easy fix (look at context attributes from base interfaces) : I'll try and fix it later – canton7 Oct 29 '19 at 08:12
  • 1
    no biggie. I just wanted to mention it. This nullable stuff is driving me crazy ;-) – gsharp Oct 29 '19 at 08:32
  • 1
    @gsharp Looking at it, you need to pass the type of the interface which defines the property -- that is, `ICommon`, not `IBusinessRelation`. Each interface defines its own `NullableContext`. I've clarified my answer, and added a runtime check for this. – canton7 Oct 29 '19 at 09:23
  • For arrays, etc. you will need to use the entire byte array to get the subtypes' nullability. I think you can also use `property.DeclaringType` instead of passing the `enclosingType`, but I haven't thoroughly tested it. – Arin Taylor Jun 04 '20 at 17:18
  • Indeed. This answer is about the nullability of the property itself, not any array elements / generic arguments. Ooh I'll have to test DeclaringType – canton7 Jun 04 '20 at 17:21
  • I still can't get this to work in the most basic scenario. I always get context 1 and nullable 0, no matter what. This is in a 3.1 project in VS 16.5.3 – Arwin Jun 30 '20 at 14:18
  • @Arwin Can you create a repro on sharplab.io ? – canton7 Jun 30 '20 at 14:18
  • Trying, but ... Unbreakable.AssemblyGuardException: Type System.Reflection.CustomAttributeData is not allowed. at Unbreakable.Internal.AssemblyValidator.EnsureAllowed(TypeReference type, String memberName) at – Arwin Jun 30 '20 at 14:27
  • https://sharplab.io/#gist:bfefdb3bdf3f8566fe8092539c535816 – Arwin Jun 30 '20 at 14:30
  • Basically that's what I did in a Unit Test eventually, to eliminate the rest as much as possible – Arwin Jun 30 '20 at 14:30
  • @Arwin So, that's got a NullableContext of 1, meaning that the property is not nullable. Which is correct. – canton7 Jun 30 '20 at 14:31
  • 1
    @Arwin Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216956/discussion-between-canton7-and-arwin). – canton7 Jun 30 '20 at 14:33
  • https://dotnetfiddle.net/wr8R3u <- if one of the properties is not nullable, the result is false. If both properties are nullable, the result is true. So something doesn't work right here or I'm missing something? – Arwin Jul 02 '20 at 10:46
  • 1
    @Arwin This is only for nullable *reference* types. You've got a value type in there. This code only works if the property is a reference type. I'll add a check. – canton7 Jul 02 '20 at 10:52
  • 1
    Yep, thanks a lot, just started to dawn on me when we chatted, and then the solution was simple. My code first checks if the underlying type is not a 'regular' nullable, and then performs this check to see if it is a nullable reference type. So here I simply return the default false if this PropertyType.IsValueType.. – Arwin Jul 02 '20 at 17:04
  • 1
    So simple and straightforward. :) – MgSam Jul 28 '20 at 21:16
  • 3
    I wrapped all this in a library, see https://github.com/RicoSuter/Namotion.Reflection – Rico Suter Sep 30 '20 at 08:33
  • @canton7 Can this be used to determine nullability of a general type, e.g. `string? s = null; IsNullable(s.GetType())`? – lonix Sep 27 '21 at 17:17
  • @lonix No, nullability of locals is just tracked by the compiler, and isn't emitted anywhere. But there's often little point in doing reflection with locals, either – canton7 Sep 28 '21 at 18:25
29

.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);
}
w5l
  • 5,341
  • 1
  • 25
  • 43
Bill Menees
  • 2,124
  • 24
  • 25
9

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

Rico Suter
  • 11,548
  • 6
  • 67
  • 93
  • 1
    Excellent work, sir. None of the other answers come close to tackling the complexity of how the attributes are implemented, but your library seems to cover all scenarios. – Timo Oct 13 '20 at 09:11
  • 2
    I'm using this library, but there are a number of incorrect (and inefficient) lock usages around caching which have the potential to fail in concurrent situations. Because of that, I *strongly* recommend managing concurrent to types exposed from this library. See https://twitter.com/casperOne/status/1388962185813102595 for an example. – casperOne May 02 '21 at 21:43
  • 1
    Funny, the reason I'm on this question is because I am writing a test to ensure all my API models have their properties marked with the appropriate `RequiredAttribute` for the clients generated with NSwag Thanks for everything Rico! We love your project. – Eric Sondergard Aug 23 '21 at 18:38
9

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

Tono Nam
  • 34,064
  • 78
  • 298
  • 470
  • 1
    this should be the accepted answer. simple and to the point. only change i made was creating NullabilityInfoContext as a static so it can be reused. is there a downside to this? – microsoftvamp May 10 '23 at 12:24
  • NullabilityInfoContext manages some internal state in Dictionaries and I don't see any locking before a call to Add so I would say that it's not thread safe. – Evil Pigeon May 29 '23 at 06:28
0

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;
    }
sjb-sjb
  • 1,112
  • 6
  • 14
  • Thanks for the heads-up. My code now checks `.DeclaringType` (recursively), and also handles `ParameterInfo` – canton7 Dec 07 '20 at 17:16
0

It's only the string? which gets a bit tricky. The rest of the nullable types are pretty straightforward to find out. For strings I used the following method, which you need to pass in a PropertyInfo object taken via reflection.

private bool IsNullable(PropertyInfo prop)
{
  return prop.CustomAttributes.Any(x => x.AttributeType.Name == "NullableAttribute");
}
Ε Г И І И О
  • 11,199
  • 1
  • 48
  • 63