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?
There are two ways to control the nullable context. At the project level, you can add the <Nullable>enable</Nullable> project setting. In a single C# source file, you can add the #nullable enable pragma to enable the nullable context.
A reference isn't supposed to be null. The default state of a nonnullable reference variable is not-null. The compiler enforces rules that ensure it's safe to dereference these variables without first checking that it isn't null: The variable must be initialized to a non-null value.
Nullable types represent value-type variables that can be assigned the value of null. You cannot create a nullable type based on a reference type.
In C# 8.0, strings are known as a nullable “string!”, and so the AllowNull annotation allows setting it to null, even though the string that we return isn't null (for example, we do a comparison check and set it to a default value if null.)
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".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With