diff --git a/README.md b/README.md
index c355f5a..a6d071e 100644
--- a/README.md
+++ b/README.md
@@ -78,13 +78,17 @@ To get more metadata from a `PropertyInfo`, you can use extensions methods like:
* `IsIndexer`
* `HasAttribute` and `HasAttributeInHierarchy`
* `IsPublic`, `IsInternal` or `IsAbstract` to check either the getter or setters matches the criteria
+* `GetNullability` and `IsNullableReference` to determine the nullable reference type annotation of the property
Similarly, you can find indexers using `FindIndexers`, conversion operators through `FindImplicitConversionOperators`
and `FindExplicitConversionOperators`, and methods via `FindMethod`, `FindParameterlessMethod` and `HasMethod`.
+For a `FieldInfo`, you can use `GetNullability` and `IsNullableReference` in the same way as for `PropertyInfo`.
+
For `ParameterInfo`, you can use:
* `HasAttribute` and `HasAttributeInHierarchy` to check whether a parameter is decorated with a specific attribute, with an optional predicate to filter on attribute properties.
+* `GetNullability` and `IsNullableReference` to determine the nullable reference type annotation of the parameter.
Other extension methods act on `Type` directly and include:
@@ -102,6 +106,12 @@ Additionally, Reflectify offers some helpers such as
* `NullableOrActualType` to get the actual type of a nullable type or the type itself if it's not nullable.
+`GetNullability` returns a `Nullability` enum (`Unknown`, `NotNull` or `Nullable`) that reflects the compiler-emitted
+nullable reference type metadata for reference types, and treats value types consistently with `NullableOrActualType`
+(e.g. `int` is `NotNull`, `int?` is `Nullable`). `IsNullableReference` is a convenience shortcut for
+`GetNullability() == Nullability.Nullable`. On .NET 6 and later this is backed by `NullabilityInfoContext`; on older
+targets it's determined by reading the compiler's `NullableAttribute`/`NullableContextAttribute` metadata directly.
+
## Download
This library is available as [a NuGet package](https://www.nuget.org/packages/Reflectify) on https://nuget.org. To install it, use the following command-line:
diff --git a/src/Reflectify/FieldInfoExtensions.cs b/src/Reflectify/FieldInfoExtensions.cs
new file mode 100644
index 0000000..1dd9362
--- /dev/null
+++ b/src/Reflectify/FieldInfoExtensions.cs
@@ -0,0 +1,63 @@
+#if !REFLECTIFY_COMPILE
+//
+#pragma warning disable
+#endif
+
+#nullable disable
+
+using System;
+using System.Reflection;
+
+namespace Reflectify;
+
+#if REFLECTIFY_COMPILE
+public static class FieldInfoExtensions
+#else
+[global::Microsoft.CodeAnalysis.Embedded]
+[global::System.Diagnostics.DebuggerNonUserCode]
+internal static class FieldInfoExtensions
+#endif
+{
+ ///
+ /// Determines the nullability of the field, taking into account nullable reference type metadata as well as
+ /// nullable value types (see ).
+ ///
+ public static Nullability GetNullability(this FieldInfo field)
+ {
+ Type fieldType = field.FieldType;
+
+ Type actualType = fieldType.NullableOrActualType();
+
+ if (actualType != fieldType)
+ {
+ return Nullability.Nullable;
+ }
+
+ if (fieldType.IsValueType)
+ {
+ return Nullability.NotNull;
+ }
+
+#if NET6_0_OR_GREATER
+ NullabilityInfo info = new NullabilityInfoContext().Create(field);
+
+ return info.ReadState switch
+ {
+ NullabilityState.NotNull => Nullability.NotNull,
+ NullabilityState.Nullable => Nullability.Nullable,
+ _ => Nullability.Unknown
+ };
+#else
+ return NullabilityMetadataReader.GetNullability(fieldType, field.GetCustomAttributes(inherit: false), field.DeclaringType);
+#endif
+ }
+
+ ///
+ /// Returns if the field is annotated as accepting , either
+ /// because it is a nullable reference type or a nullable value type, or otherwise.
+ ///
+ public static bool IsNullableReference(this FieldInfo field)
+ {
+ return field.GetNullability() == Nullability.Nullable;
+ }
+}
diff --git a/src/Reflectify/Nullability.cs b/src/Reflectify/Nullability.cs
new file mode 100644
index 0000000..9c14892
--- /dev/null
+++ b/src/Reflectify/Nullability.cs
@@ -0,0 +1,36 @@
+#if !REFLECTIFY_COMPILE
+//
+#pragma warning disable
+#endif
+
+#nullable disable
+
+namespace Reflectify;
+
+///
+/// Represents the nullability of a member, as determined from the compiler-emitted nullable reference type
+/// metadata, or from the fact that the member is a value type (or ).
+///
+#if REFLECTIFY_COMPILE
+public enum Nullability
+#else
+[global::Microsoft.CodeAnalysis.Embedded]
+internal enum Nullability
+#endif
+{
+ ///
+ /// The nullability of the member could not be determined, typically because it was compiled without
+ /// nullable reference types enabled (the "oblivious" context).
+ ///
+ Unknown,
+
+ ///
+ /// The member is annotated as not allowing .
+ ///
+ NotNull,
+
+ ///
+ /// The member is annotated as allowing .
+ ///
+ Nullable
+}
diff --git a/src/Reflectify/NullabilityMetadataReader.cs b/src/Reflectify/NullabilityMetadataReader.cs
new file mode 100644
index 0000000..5c0f62e
--- /dev/null
+++ b/src/Reflectify/NullabilityMetadataReader.cs
@@ -0,0 +1,149 @@
+#if !REFLECTIFY_COMPILE
+//
+#pragma warning disable
+#endif
+
+#nullable disable
+
+using System;
+using System.Reflection;
+
+namespace Reflectify;
+
+// This fallback is only needed on frameworks that lack System.Reflection.NullabilityInfoContext (pre-.NET 6). On
+// net6.0-or-greater the extension methods use that API directly, so this whole type is excluded there to avoid it
+// being reported as uninstrumented/uncovered dead code.
+#if !NET6_0_OR_GREATER
+///
+/// Provides the manual, attribute-based fallback used to determine nullable reference type metadata on target
+/// frameworks that don't have access to System.Reflection.NullabilityInfoContext (i.e. everything
+/// before .NET 6).
+///
+///
+/// The C# compiler encodes nullable reference type information using two compiler-internal attributes that are not
+/// part of the public BCL surface: System.Runtime.CompilerServices.NullableAttribute and
+/// System.Runtime.CompilerServices.NullableContextAttribute. Because these attributes are embedded
+/// per-assembly (rather than shared through a common reference), they must be matched by their full type name
+/// instead of via typeof(...), since a NullableAttribute emitted into one assembly is a different CLR
+/// type than one emitted into another. See
+/// https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md for the specification this logic
+/// is based on.
+///
+#if !REFLECTIFY_COMPILE
+[global::Microsoft.CodeAnalysis.Embedded]
+#endif
+[global::System.Diagnostics.DebuggerNonUserCode]
+internal static class NullabilityMetadataReader
+{
+ private const string NullableAttributeFullName = "System.Runtime.CompilerServices.NullableAttribute";
+ private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute";
+
+ ///
+ /// Determines the of a member of type , given the
+ /// attributes declared directly on that member and the type that declares it.
+ ///
+ /// The type of the property, field or parameter.
+ /// The custom attributes declared directly on the member.
+ /// The type that declares the member, used to walk outward for the ambient nullable context.
+ public static Nullability GetNullability(Type memberType, object[] memberAttributes, Type declaringType)
+ {
+ if (memberType is null)
+ {
+ return Nullability.Unknown;
+ }
+
+ Type actualType = memberType.NullableOrActualType();
+
+ if (actualType != memberType)
+ {
+ // Nullable, e.g. int?
+ return Nullability.Nullable;
+ }
+
+ if (memberType.IsValueType)
+ {
+ // Plain value types, e.g. int, can never be null.
+ return Nullability.NotNull;
+ }
+
+ byte? flag = GetNullableAttributeFlag(memberAttributes) ?? GetAmbientNullableContextFlag(declaringType);
+
+ return MapFlag(flag);
+ }
+
+ private static byte? GetNullableAttributeFlag(object[] attributes)
+ {
+ foreach (object attribute in attributes)
+ {
+ if (attribute.GetType().FullName == NullableAttributeFullName)
+ {
+ FieldInfo field = attribute.GetType().GetField("NullableFlags", BindingFlags.Public | BindingFlags.Instance);
+
+ // The NullableAttribute constructor always populates NullableFlags as a byte[], regardless of
+ // whether it was invoked with a single byte or a byte[] (nested generic arguments). The first
+ // element always applies to the member itself.
+ if (field?.GetValue(attribute) is byte[] { Length: > 0 } flags)
+ {
+ return flags[0];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static byte? GetAmbientNullableContextFlag(Type declaringType)
+ {
+ for (Type type = declaringType; type is not null; type = type.DeclaringType)
+ {
+ byte? flag = GetNullableContextFlag(type.GetCustomAttributes(inherit: false));
+
+ if (flag is not null)
+ {
+ return flag;
+ }
+ }
+
+ if (declaringType is not null)
+ {
+ byte? flag = GetNullableContextFlag(declaringType.Module.GetCustomAttributes(inherit: false));
+
+ if (flag is not null)
+ {
+ return flag;
+ }
+ }
+
+ return null;
+ }
+
+ private static byte? GetNullableContextFlag(object[] attributes)
+ {
+ foreach (object attribute in attributes)
+ {
+ if (attribute.GetType().FullName == NullableContextAttributeFullName)
+ {
+ FieldInfo field = attribute.GetType().GetField("Flag", BindingFlags.Public | BindingFlags.Instance);
+
+ if (field?.GetValue(attribute) is byte value)
+ {
+ return value;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static Nullability MapFlag(byte? flag)
+ {
+ // Per the Roslyn nullable metadata spec: 0 = oblivious, 1 = not annotated (not null), 2 = annotated (nullable).
+ return flag switch
+ {
+ 1 => Nullability.NotNull,
+ 2 => Nullability.Nullable,
+ _ => Nullability.Unknown
+ };
+ }
+}
+#endif
diff --git a/src/Reflectify/ParameterInfoExtensions.cs b/src/Reflectify/ParameterInfoExtensions.cs
index 3dd82aa..3aaf49e 100644
--- a/src/Reflectify/ParameterInfoExtensions.cs
+++ b/src/Reflectify/ParameterInfoExtensions.cs
@@ -53,4 +53,48 @@ public static bool HasAttributeInHierarchy(this ParameterInfo parame
{
return parameter.IsDefined(typeof(TAttribute), inherit: true);
}
+
+ ///
+ /// Determines the nullability of the parameter, taking into account nullable reference type metadata as well as
+ /// nullable value types (see ).
+ ///
+ public static Nullability GetNullability(this ParameterInfo parameter)
+ {
+ Type parameterType = parameter.ParameterType;
+
+ Type actualType = parameterType.NullableOrActualType();
+
+ if (actualType != parameterType)
+ {
+ return Nullability.Nullable;
+ }
+
+ if (parameterType.IsValueType)
+ {
+ return Nullability.NotNull;
+ }
+
+#if NET6_0_OR_GREATER
+ NullabilityInfo info = new NullabilityInfoContext().Create(parameter);
+
+ return info.ReadState switch
+ {
+ NullabilityState.NotNull => Nullability.NotNull,
+ NullabilityState.Nullable => Nullability.Nullable,
+ _ => Nullability.Unknown
+ };
+#else
+ return NullabilityMetadataReader.GetNullability(parameterType, parameter.GetCustomAttributes(inherit: false),
+ parameter.Member.DeclaringType);
+#endif
+ }
+
+ ///
+ /// Returns if the parameter is annotated as accepting , either
+ /// because it is a nullable reference type or a nullable value type, or otherwise.
+ ///
+ public static bool IsNullableReference(this ParameterInfo parameter)
+ {
+ return parameter.GetNullability() == Nullability.Nullable;
+ }
}
diff --git a/src/Reflectify/PropertyInfoExtensions.cs b/src/Reflectify/PropertyInfoExtensions.cs
index e049f95..6328578 100644
--- a/src/Reflectify/PropertyInfoExtensions.cs
+++ b/src/Reflectify/PropertyInfoExtensions.cs
@@ -5,6 +5,7 @@
#nullable disable
+using System;
using System.Reflection;
namespace Reflectify;
@@ -64,4 +65,48 @@ public static bool IsAbstract(this PropertyInfo prop)
{
return prop.GetMethod is { IsAbstract: true } || prop.SetMethod is { IsAbstract: true };
}
+
+ ///
+ /// Determines the nullability of the property, taking into account nullable reference type metadata as well as
+ /// nullable value types (see ).
+ ///
+ public static Nullability GetNullability(this PropertyInfo prop)
+ {
+ Type propertyType = prop.PropertyType;
+
+ Type actualType = propertyType.NullableOrActualType();
+
+ if (actualType != propertyType)
+ {
+ return Nullability.Nullable;
+ }
+
+ if (propertyType.IsValueType)
+ {
+ return Nullability.NotNull;
+ }
+
+#if NET6_0_OR_GREATER
+ NullabilityInfo info = new NullabilityInfoContext().Create(prop);
+ NullabilityState state = prop.GetMethod is not null ? info.ReadState : info.WriteState;
+
+ return state switch
+ {
+ NullabilityState.NotNull => Nullability.NotNull,
+ NullabilityState.Nullable => Nullability.Nullable,
+ _ => Nullability.Unknown
+ };
+#else
+ return NullabilityMetadataReader.GetNullability(propertyType, prop.GetCustomAttributes(inherit: false), prop.DeclaringType);
+#endif
+ }
+
+ ///
+ /// Returns if the property is annotated as accepting , either
+ /// because it is a nullable reference type or a nullable value type, or otherwise.
+ ///
+ public static bool IsNullableReference(this PropertyInfo prop)
+ {
+ return prop.GetNullability() == Nullability.Nullable;
+ }
}
diff --git a/tests/Reflectify.Specs/FieldInfoExtensionsSpecs.cs b/tests/Reflectify.Specs/FieldInfoExtensionsSpecs.cs
new file mode 100644
index 0000000..395222e
--- /dev/null
+++ b/tests/Reflectify.Specs/FieldInfoExtensionsSpecs.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using System.Reflection;
+using FluentAssertions;
+using JetBrains.Annotations;
+using Xunit;
+
+namespace Reflectify.Specs;
+
+public class FieldInfoExtensionsSpecs
+{
+ public class GetNullability
+ {
+ [Fact]
+ public void A_non_nullable_reference_field_is_not_null()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NonNullableString");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_reference_field_is_nullable()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NullableString");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_value_type_field_is_not_null()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NonNullableInt");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_value_type_field_is_nullable()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NullableInt");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_non_nullable_generic_field_is_not_null()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NonNullableList");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_generic_field_is_nullable()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NullableList");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_field_compiled_without_a_nullable_context_is_unknown()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithoutNullableContext).GetField("SomeString");
+
+ // Assert
+ field.GetNullability().Should().Be(Nullability.Unknown);
+ }
+ }
+
+ public class IsNullableReference
+ {
+ [Fact]
+ public void A_nullable_reference_field_is_a_nullable_reference()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NullableString");
+
+ // Assert
+ field.IsNullableReference().Should().BeTrue();
+ }
+
+ [Fact]
+ public void A_non_nullable_reference_field_is_not_a_nullable_reference()
+ {
+ // Act
+ FieldInfo field = typeof(ClassWithNullableFields).GetField("NonNullableString");
+
+ // Assert
+ field.IsNullableReference().Should().BeFalse();
+ }
+ }
+
+#nullable enable
+#pragma warning disable CS0649 // Field is never assigned to - these fields exist only for reflection metadata purposes.
+ private class ClassWithNullableFields
+ {
+ [UsedImplicitly]
+ public string NonNullableString = "";
+
+ [UsedImplicitly]
+ public string? NullableString;
+
+ [UsedImplicitly]
+ public int NonNullableInt;
+
+ [UsedImplicitly]
+ public int? NullableInt;
+
+ [UsedImplicitly]
+ public List NonNullableList = new();
+
+ [UsedImplicitly]
+ public List? NullableList;
+ }
+#pragma warning restore CS0649
+#nullable disable
+
+ private class ClassWithoutNullableContext
+ {
+ [UsedImplicitly]
+#pragma warning disable CS0649 // Field is never assigned to - this field exists only for reflection metadata purposes.
+ public string SomeString;
+#pragma warning restore CS0649
+ }
+}
diff --git a/tests/Reflectify.Specs/ParameterInfoExtensionsSpecs.cs b/tests/Reflectify.Specs/ParameterInfoExtensionsSpecs.cs
index 7008213..757b8ab 100644
--- a/tests/Reflectify.Specs/ParameterInfoExtensionsSpecs.cs
+++ b/tests/Reflectify.Specs/ParameterInfoExtensionsSpecs.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
using FluentAssertions;
+using JetBrains.Annotations;
using Xunit;
namespace Reflectify.Specs;
@@ -99,4 +101,123 @@ public void Method([CustomParameter("Specific reason")] string value)
{
}
}
+
+ public class GetNullability
+ {
+ [Fact]
+ public void A_non_nullable_reference_parameter_is_not_null()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("Method")!.GetParameters()[0];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_reference_parameter_is_nullable()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("Method")!.GetParameters()[1];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_value_type_parameter_is_not_null()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("Method")!.GetParameters()[2];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_value_type_parameter_is_nullable()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("OtherMethod")!.GetParameters()[0];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_non_nullable_generic_parameter_is_not_null()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("OtherMethod")!.GetParameters()[1];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_generic_parameter_is_nullable()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("OtherMethod")!.GetParameters()[2];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_parameter_compiled_without_a_nullable_context_is_unknown()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithoutNullableContext).GetMethod("Method")!.GetParameters()[0];
+
+ // Act / Assert
+ parameter.GetNullability().Should().Be(Nullability.Unknown);
+ }
+ }
+
+ public class IsNullableReference
+ {
+ [Fact]
+ public void A_nullable_reference_parameter_is_a_nullable_reference()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("Method")!.GetParameters()[1];
+
+ // Act / Assert
+ parameter.IsNullableReference().Should().BeTrue();
+ }
+
+ [Fact]
+ public void A_non_nullable_reference_parameter_is_not_a_nullable_reference()
+ {
+ // Arrange
+ ParameterInfo parameter = typeof(ClassWithNullableParameters).GetMethod("Method")!.GetParameters()[0];
+
+ // Act / Assert
+ parameter.IsNullableReference().Should().BeFalse();
+ }
+ }
+
+#nullable enable
+ private class ClassWithNullableParameters
+ {
+ [UsedImplicitly]
+ public void Method(string nonNullableString, string? nullableString, int nonNullableInt)
+ {
+ }
+
+ [UsedImplicitly]
+ public void OtherMethod(int? nullableInt, List nonNullableList, List? nullableList)
+ {
+ }
+ }
+#nullable disable
+
+ private class ClassWithoutNullableContext
+ {
+ [UsedImplicitly]
+ public void Method(string someString)
+ {
+ }
+ }
}
diff --git a/tests/Reflectify.Specs/PropertyInfoExtensionsSpecs.cs b/tests/Reflectify.Specs/PropertyInfoExtensionsSpecs.cs
index 67a9307..2eb5059 100644
--- a/tests/Reflectify.Specs/PropertyInfoExtensionsSpecs.cs
+++ b/tests/Reflectify.Specs/PropertyInfoExtensionsSpecs.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using FluentAssertions;
@@ -195,6 +196,102 @@ private sealed class ConcreteClassWithProperty : AbstractClassWithProperty
}
}
+ public class GetNullability
+ {
+ [Fact]
+ public void A_non_nullable_reference_property_is_not_null()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NonNullableString");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_reference_property_is_nullable()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NullableString");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_value_type_property_is_not_null()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NonNullableInt");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_value_type_property_is_nullable()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NullableInt");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_non_nullable_generic_property_is_not_null()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NonNullableList");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.NotNull);
+ }
+
+ [Fact]
+ public void A_nullable_generic_property_is_nullable()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NullableList");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.Nullable);
+ }
+
+ [Fact]
+ public void A_property_compiled_without_a_nullable_context_is_unknown()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithoutNullableContext).GetProperty("SomeString");
+
+ // Assert
+ property.GetNullability().Should().Be(Nullability.Unknown);
+ }
+ }
+
+ public class IsNullableReference
+ {
+ [Fact]
+ public void A_nullable_reference_property_is_a_nullable_reference()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NullableString");
+
+ // Assert
+ property.IsNullableReference().Should().BeTrue();
+ }
+
+ [Fact]
+ public void A_non_nullable_reference_property_is_not_a_nullable_reference()
+ {
+ // Act
+ PropertyInfo property = typeof(ClassWithNullableProperties).GetProperty("NonNullableString");
+
+ // Assert
+ property.IsNullableReference().Should().BeFalse();
+ }
+ }
+
private class ClassWithVariousProperties
{
[UsedImplicitly]
@@ -209,4 +306,33 @@ private class ClassWithVariousProperties
[UsedImplicitly]
protected internal string ProtectedInternalProperty { get; set; }
}
+
+#nullable enable
+ private class ClassWithNullableProperties
+ {
+ [UsedImplicitly]
+ public string NonNullableString { get; set; } = "";
+
+ [UsedImplicitly]
+ public string? NullableString { get; set; }
+
+ [UsedImplicitly]
+ public int NonNullableInt { get; set; }
+
+ [UsedImplicitly]
+ public int? NullableInt { get; set; }
+
+ [UsedImplicitly]
+ public List NonNullableList { get; set; } = new();
+
+ [UsedImplicitly]
+ public List? NullableList { get; set; }
+ }
+#nullable disable
+
+ private class ClassWithoutNullableContext
+ {
+ [UsedImplicitly]
+ public string SomeString { get; set; }
+ }
}