How to use Expression Tree to safely access path of nullable objects?

c# expression-trees lambda path tree

Question

When I get deserialized XML result into xsd-generated tree of objects and want to use some deep object inside that tree a.b.c.d.e.f, it will give me exception if any node on that query path is missing.

if(a.b.c.d.e.f != null)
    Console.Write("ok");

I want to avoid checking for null for each level like this:

if(a != null)
if(a.b != null)
if(a.b.c != null)
if(a.b.c.d != null)
if(a.b.c.d.e != null)
if(a.b.c.d.e.f != null)
    Console.Write("ok");

First solution is to implement Get extension method that allows this:

if(a.Get(o=>o.b).Get(o=>o.c).Get(o=>o.d).Get(o=>o.e).Get(o=>o.f) != null)
    Console.Write("ok");

Second solution is to implement Get(string) extension method and use reflection to get result looking like this:

if(a.Get("b.c.d.e.f") != null)
    Console.Write("ok");

Third solution, could be to implement ExpandoObject and use dynamic type to get result looking like this:

dynamic da = new SafeExpando(a);
if(da.b.c.d.e.f != null)
    Console.Write("ok");

But last 2 solutions do not give benefits of strong typing and IntelliSense.

I think the best could be fourth solution that can be implemented with Expression Trees:

if(Get(a.b.c.d.e.f) != null)
    Console.Write("ok");

or

if(a.Get(a=>a.b.c.d.e.f) != null)
    Console.Write("ok");

I already implemented 1st and 2nd solutions.

Here is how 1st solution looks like:

[DebuggerStepThrough]
public static To Get<From,To>(this From @this, Func<From,To> get)
{
    var ret = default(To);
    if(@this != null && !@this.Equals(default(From)))
        ret = get(@this);

    if(ret == null && typeof(To).IsArray)
        ret = (To)Activator.CreateInstance(typeof(To), 0);

    return ret;
}

How to implement 4th solution if possible ?

Also it would be interesting to see how to implement 3rd solution if possible.

Accepted Answer

So the starting place is creating an expression visitor. This lets us find all of the member accesses within a particular expression. This leaves us with the question of what to do for each member access.

So the first thing is to recursively visit on the expression that the member is being accessed on. From there, we can use Expression.Condition to create a conditional block that compares that processed underlying expression to null, and returns null if true an the original starting expression if it's not.

Note that we need to provide implementations for both Members and method calls, but the process for each is basically identical.

We'll also add in a check so that of the underlying expression is null (which is to say, there is no instance and it's a static member) or if it's a non-nullable type, that we just use the base behavior instead.

public class MemberNullPropogationVisitor : ExpressionVisitor
{
    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == null || !IsNullable(node.Expression.Type))
            return base.VisitMember(node);

        var expression = base.Visit(node.Expression);
        var nullBaseExpression = Expression.Constant(null, expression.Type);
        var test = Expression.Equal(expression, nullBaseExpression);
        var memberAccess = Expression.MakeMemberAccess(expression, node.Member);
        var nullMemberExpression = Expression.Constant(null, node.Type);
        return Expression.Condition(test, nullMemberExpression, node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Object == null || !IsNullable(node.Object.Type))
            return base.VisitMethodCall(node);

        var expression = base.Visit(node.Object);
        var nullBaseExpression = Expression.Constant(null, expression.Type);
        var test = Expression.Equal(expression, nullBaseExpression);
        var memberAccess = Expression.Call(expression, node.Method);
        var nullMemberExpression = Expression.Constant(null, MakeNullable(node.Type));
        return Expression.Condition(test, nullMemberExpression, node);
    }

    private static Type MakeNullable(Type type)
    {
        if (IsNullable(type))
            return type;

        return typeof(Nullable<>).MakeGenericType(type);
    }

    private static bool IsNullable(Type type)
    {
        if (type.IsClass)
            return true;
        return type.IsGenericType &&
            type.GetGenericTypeDefinition() == typeof(Nullable<>);
    }
}

We can then create an extension method to make calling it easier:

public static Expression PropogateNull(this Expression expression)
{
    return new MemberNullPropogationVisitor().Visit(expression);
}

As well as one that accepts a lambda, rather than any expression, and can return a compiled delegate:

public static Func<T> PropogateNull<T>(this Expression<Func<T>> expression)
{
    var defaultValue = Expression.Constant(default(T));
    var body = expression.Body.PropogateNull();
    if (body.Type != typeof(T))
        body = Expression.Coalesce(body, defaultValue);
    return Expression.Lambda<Func<T>>(body, expression.Parameters)
        .Compile();
}

Note that, to support cases where the accessed member resolves to a non-nullable value, we're changing the type of those expressions to lift them to be nullable, using MakeNullable. This is a problem with this final expression, as it needs to be a Func<T>, and it won't match if T isn't also lifted. Thus, while it's very much non-ideal (ideally you'd never call this method with a non-nullable T, but there's no good way to support this in C#) we coalesce the final value using the default value for that type, if necessary.

(You can trivially modify this to accept a lambda accepting a parameter, and pass in a value, but you can just as easily close over that parameter instead, so I see no real reason to.)


It's also worth pointing out that in C# 6.0, when it's actually released, we'll have an actual null propogation operator (?.), making all of this very unnecessary. You'll be able to write:

if(a?.b?.c?.d?.e?.f != null)
    Console.Write("ok");

and have exactly the semantics you're looking for.



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow