Search code examples
c#reflectionnullablenullable-reference-types

How to check for nullable while using a property selector


Given the following class

   public class Anonymizer : IAnonymizer
{
    public void Anonymize<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> propertySelector)
    {
        var value = typeof(TProperty) switch
        {
            { } t when IsNullable(t)  => (TProperty?)(object) null!,
            { } t when t == typeof(string) => (TProperty) (object) "<Anonymized>",
            { } t when t == typeof(int) => (TProperty) (object) -1,
            _ => default,
        };
        
        var memberExpression = (MemberExpression)propertySelector.Body;
        var property = (PropertyInfo)memberExpression.Member;
        property.SetValue(entity, value);
    }

    private static bool IsNullable(Type t)
    {
        return !t.IsValueType ||  Nullable.GetUnderlyingType(t) != null;
    }
}

and the following tests

  [Fact]
    public void Test_string()
    {
        var sut = new Anonymizer();
        
        var myObject = new MyClass {MyText = "Hello World", MyNumber = 42};

        sut.Anonymize(myObject, e => e.MyText);

        myObject.MyText.Should().Be("<Anonymized>");
        myObject.MyNumber.Should().Be(42);
    }

    
    [Fact]
    public void Test_int()
    {
        var sut = new Anonymizer();
        
        var myObject = new MyClass {MyText = "Hello World", MyNumber = 42};

        sut.Anonymize(myObject, e => e.MyNumber);

        myObject.MyText.Should().Be("Hello World");
        myObject.MyNumber.Should().Be(-1);
    }
    
        
    [Fact]
    public void Test_NullableString()
    {
        var sut = new Anonymizer();
        
        var myObject = new MyClass
        {
            MyText = "Hello World", 
            MyNumber = 42,
            MyNullableString = "Hello World"
        };

        sut.Anonymize(myObject, e => e.MyNullableString);
        
        myObject.MyNumber.Should().Be(42);
        myObject.MyNullableString.Should().BeNull();
    }

    class MyClass
    {
        public string MyText { get; set; }
        public int MyNumber { get; set; }
        public string? MyNullableString { get; set; }
    }

It seems like there is no way to distinguish between a property of type "string" and "string?". It seems like TProperty will be "string" in both cases.

Any suggestions


Solution

  • You need to analyze the NullabilityInfoContext, something along these lines:

    public class Anonymizer : IAnonymizer
    {
        public void Anonymize<TEntity, TProperty>(TEntity entity, Expression<Func<TEntity, TProperty>> propertySelector)
        {
            var value = typeof(TProperty) switch
            {
                { } t when IsNullableValueType(t)  => (TProperty?)(object) null!,
                { } t when IsNullableReferenceType(propertySelector)  => (TProperty?)(object) null!,
                // ...
            };
            
            // ...
        }
    
        private static bool IsNullableValueType(Type t) => Nullable.GetUnderlyingType(t) != null;
        
        private static bool IsNullableReferenceType<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> propertySelector)
        {
            if (typeof(TProperty).IsValueType) return false;
            var memberExpression = (MemberExpression)propertySelector.Body;
            var property = (PropertyInfo)memberExpression.Member;
    
            NullabilityInfoContext context = new();
            var nullabilityInfo = context.Create(property); 
            return nullabilityInfo.ReadState == NullabilityState.Nullable;
        }
    }
    

    Note that invoking reflection in this manner every time can be costly (or even not available in some cases), so you should consider following options:

    • Using source generators (without better overview of codebase and use cases it is hard to tell if it is feasible)
    • Cache reflection (see one of the following answers: one, two, three to get some ideas)