Ibanez
Ibanez

Reputation: 409

Handle null values of nested objects in C# expression tree

I have searched, and found similar posts pertaining to my issue, however nothing seems to solve my problem.

I am fairly new to C#, and this is my first attempt at building an expression tree. (please go easy ;-)

I am trying to create an expression tree which would, once compiled, filter values on a set of data.

Here is my expression method:

private static Expression<Func<TItem, bool>> CreateFilterExpression<TItem>(string propertyName, string expressionType, dynamic filterValue)
{
    if (param == null)
    {
        param = Expression.Parameter(typeof(TItem), "item");
    }
    MemberExpression member = GetMemberExpression<TItem>(propertyName);

    //When we call our method, we need to evaluate on the same type
    //we convert the filter value to the type of the property we are evaluating on
    dynamic convertedValue = Convert.ChangeType(filterValue, member.Type);
    MethodInfo method = member.Type.GetMethod(expressionType, new[] { member.Type });
    ConstantExpression constantValue = Expression.Constant(convertedValue, member.Type);
    Expression containsMethodExp;

    if (expressionType == "NotEqual")
    {
        method = member.Type.GetMethod("Equals", new[] { member.Type });
    }
    if (member.Type.ToString().ToLower() == "system.string")
    {
        //We need to compare the lower case of property and value
        MethodCallExpression propertyValueToLowerCase = Expression.Call(member, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
        MethodCallExpression filterValueToLowerCase = Expression.Call(constantValue, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
        containsMethodExp = Expression.Call(propertyValueToLowerCase, method, filterValueToLowerCase);
    }
    else if (member.Type.ToString().ToLower() == "system.datetime")
    {
        //we need to compare only the dates
        MemberExpression dateOnlyProperty = Expression.Property(member, "Date");
        containsMethodExp = Expression.Call(dateOnlyProperty, method, constantValue);
    }
    else
    {
        containsMethodExp = Expression.Call(member, method, constantValue);
    }

    if (expressionType == "NotEqual")
    {
        containsMethodExp = Expression.Not(containsMethodExp);
    }

    return Expression.Lambda<Func<TItem, bool>>(containsMethodExp, param);
}

private static MemberExpression GetMemberExpression<TItem>(string propertyName)
{
    if (param == null)
    {
        param = Expression.Parameter(typeof(TItem), "item");
    }
    MemberExpression member = null;

    //Check if we have a nested property
    if (propertyName.Contains('.'))
    {
        Expression nestedProperty = param;
        string[] properies = propertyName.Split('.');
        int zeroIndex = properies.Count() - 1;
        for (int i = 0; i <= zeroIndex; i++)
        {
            if (i < zeroIndex)
            {
                nestedProperty = Expression.PropertyOrField(nestedProperty, properies[i]);
            }
            else
            {
                member = Expression.Property(nestedProperty, properies[i]);
            }
        }
    }
    else
    {
        member = Expression.Property(param, propertyName);
    }
    return member;
}

Example usage would be like so:

var lambda = CreateFilterExpression<T>("Some.Nested.Object", "Equals", "Some value");
var compiled = lambda.Compile();
gridData = gridData.Where(compiled);

An example of the data I trying to ultimately bind to my grid looks like this:

public class Some : BaseClass
{
    public decimal NumberAvailable { get; set; }
    public DateTime EffectiveDate { get; set; }
    public Batch Batch { get; set; }
    public decimal Price { get; set; }
    public decimal Limit { get; set; }
    public NestedClass Nested { get; set; }
    public int? CompanyId { get; set; }
    public decimal Amount { get; set; }
}

public class NestedClass : BaseClass
{
    public int RequestId { get; set; }
    public string Code { get; set; }
    public string Company { get; set; }
    public string Reference { get; set; }
}

The problem occurs when we have null value on an object, like "Some.Nested = null", and then trying to convert "Reference" to lowercase. Here:

MethodCallExpression propertyValueToLowerCase = Expression.Call(member, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));

Here is the result in debugger:

enter image description here

How can I check for null values, on nested objects, and return empty string if it is null?

I hope I explained my question well enough. Thank you in advance!

Upvotes: 3

Views: 4122

Answers (1)

poke
poke

Reputation: 387707

What you want to do is to generate an expression like this:

Some == null ? null : Some.Nested == null ? null : Some.Nested.Object

This unfortunately is no longer a member expression, so GetMemberExpression wouldn’t work for this. Instead you need a chain of conditional expression that accesses one more level at a time.

Once you have that, you could then do <memberExpression> ?? string.Empty to get a string which you can safely operate on.

To generate the latter expression, you can use Expression.Coalesce:

Expression.Coalesce(memberExpression, Expression.Constant(string.Empty))

For the member expression itself, you could write something like this:

Expression AccessMember(Expression obj, string propertyName)
{
    string[] parts = propertyName.Split(new char[] { '.' }, 2);
    Expression member = Expression.PropertyOrField(obj, parts[0]);

    if (parts.Length > 1)
        member = AccessMember(member, parts[1]);

    return Expression.Condition(Expression.Equal(obj, Expression.Constant(null)),
        Expression.Constant(null, member.Type), member);
}

This can be used like this:

string path = "Some.Nested.Object";
string[] parts = path.Split(new char[] { '.' }, 2);
ParameterExpression param = Expression.Parameter(typeof(T), parts[0]);
Expression memberAccess = AccessMember(param, parts[1]);

memberAccess would then be exactly the above chained conditional expression.

Combined into your function (simplified only for strings for now), it could look like this:

Expression<Func<TObj, bool>> BuildFilterExpression<TObj, TMember>(string propertyPath, TMember comparisonValue, TMember defaultValue)
{
    string[] parts = propertyPath.Split(new char[] { '.' }, 2);
    ParameterExpression param = Expression.Parameter(typeof(TObj), parts[0]);

    // get member access expression
    Expression memberExpression = AccessMember(param, parts[1]);

    // coalesce the member with the default value
    memberExpression = Expression.Coalesce(memberExpression, Expression.Constant(defaultValue));

    // get the comparison value as expression
    Expression comparisonExpression = Expression.Constant(comparisonValue);

    // type specific logic
    if (memberExpression.Type == typeof(string))
    {
        MethodInfo toLowerMethod = typeof(string).GetMethod("ToLower", Type.EmptyTypes);
        memberExpression = Expression.Call(memberExpression, toLowerMethod);
        comparisonExpression = Expression.Call(comparisonExpression, toLowerMethod);
    }

    // create the comparison expression
    Expression filterExpression = Expression.Equal(memberExpression, comparisonExpression);

    return Expression.Lambda<Func<TObj, bool>>(filterExpression, param);
}

Used like this:

BuildFilterExpression<SomeType, string>("Some.Nested.Object", "foo bar", string.Empty)

… it essentially creates the following lambda expression:

(Some) => ((Some == null ? null : Some.Nested == null ? null : Some.Nested.Object) ?? string.Empty).ToLower() == "foo bar"

Above code assumes that for a property expression Some.Nested.Object, Some is the object that is being passed to the lambda, so the first property that would be accessed is Nested. The reason is that I simply didn’t know your example object structure, so I had to come up with something.

If you want Some be the first property that is accessed for the passed object, you can easily change that though. To do that, modify the beginning of BuildFilterExpression so that the propertyPath is not split up. Pass some random name (or no name even) to Expression.Parameter, and pass the full propertyPath to AccessMember:

// don’t split up the propertyPath

// let’s call the parameter `obj`
ParameterExpression param = Expression.Parameter(typeof(TObj), "obj");

// get member access expression—for the full property path
Expression memberExpression = AccessMember(param, propertyPath);

Upvotes: 7

Related Questions