How to check for nullable while using a property selector

102 Views Asked by At

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

1

There are 1 best solutions below

0
On BEST ANSWER

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)