Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I break down a chain of member access expressions?

Tags:

The Short Version (TL;DR):

Suppose I have an expression that's just a chain of member access operators:

Expression<Func<Tx, Tbaz>> e = x => x.foo.bar.baz; 

You can think of this expression as a composition of sub-expressions, each comprising one member-access operation:

Expression<Func<Tx, Tfoo>>   e1 = (Tx x) => x.foo; Expression<Func<Tfoo, Tbar>> e2 = (Tfoo foo) => foo.bar; Expression<Func<Tbar, Tbaz>> e3 = (Tbar bar) => bar.baz; 

What I want to do is break e down into these component sub-expressions so I can work with them individually.

The Even Shorter Version:

If I have the expression x => x.foo.bar, I already know how to break off x => x.foo. How can I pull out the other sub-expression, foo => foo.bar?

Why I'm Doing This:

I'm trying to simulate "lifting" the member access operator in C#, like CoffeeScript's existential access operator ?.. Eric Lippert has stated that a similar operator was considered for C#, but there was no budget to implement it.

If such an operator existed in C#, you could do something like this:

value = target?.foo?.bar?.baz; 

If any part of the target.foo.bar.baz chain turned out to be null, then this whole thing would evaluate to null, thus avoiding a NullReferenceException.

I want a Lift extension method that can simulate this sort of thing:

value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null 

What I've Tried:

I've got something that compiles, and it sort of works. However, it's incomplete because I only know how to keep the left side of a member access expression. I can turn x => x.foo.bar.baz into x => x.foo.bar, but I don't know how to keep bar => bar.baz.

So it ends up doing something like this (pseudocode):

return (x => x)(target) == null ? null        : (x => x.foo)(target) == null ? null        : (x => x.foo.bar)(target) == null ? null        : (x => x.foo.bar.baz)(target); 

This means that the leftmost steps in the expression get evaluated over and over again. Maybe not a big deal if they're just properties on POCO objects, but turn them into method calls and the inefficiency (and potential side effects) become a lot more obvious:

//still pseudocode return (x => x())(target) == null ? null        : (x => x().foo())(target) == null ? null        : (x => x().foo().bar())(target) == null ? null        : (x => x().foo().bar().baz())(target); 

The Code:

static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)     where TResult : class {     //omitted: if target can be null && target == null, just return null      var memberExpression = exp.Body as MemberExpression;     if (memberExpression != null)     {         //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo}         var innerExpression = memberExpression.Expression;         var innerLambda = Expression.Lambda<Func<T, object>>(                               innerExpression,                                exp.Parameters                           );            if (target.Lift(innerLambda) == null)         {             return null;         }         else         {             ////This is the part I'm stuck on. Possible pseudocode:             //var member = memberExpression.Member;                           //return GetValueOfMember(target.Lift(innerLambda), member);         }     }      //For now, I'm stuck with this:     return exp.Compile()(target); } 

This was loosely inspired by this answer.


Alternatives to a Lift Method, and Why I Can't Use Them:

The Maybe monad

value = x.ToMaybe()          .Bind(y => y.foo)          .Bind(f => f.bar)          .Bind(b => b.baz)          .Value; 
Pros:
  1. Uses an existing pattern that's popular in functional programming
  2. Has other uses besides lifted member access
Cons:
  1. It's too verbose. I don't want a massive chain of function calls every time I want to drill a few members down. Even if I implement SelectMany and use the query syntax, IMHO that will look more messy, not less.
  2. I have to manually rewrite x.foo.bar.baz as its individual components, which means I have to know what they are at compile time. I can't just use an expression from a variable like result = Lift(expr, obj);.
  3. Not really designed for what I'm trying to do, and doesn't feel like a perfect fit.

ExpressionVisitor

I modified Ian Griffith's LiftMemberAccessToNull method into a generic extension method that can be used as I've described. The code is too long to include here, but I'll post a Gist if anyone's interested.

Pros:
  1. Follows the result = target.Lift(x => x.foo.bar.baz) syntax
  2. Works great if every step in the chain returns a reference type or a non-nullable value type
Cons:
  1. It chokes if any member in the chain is a nullable value type, which really limits its usefulness to me. I need it to work for Nullable<DateTime> members.

Try/catch

try  {      value = x.foo.bar.baz;  } catch (NullReferenceException ex)  {      value = null;  } 

This is the most obvious way, and it's what I'll use if I can't find a more elegant way.

Pros:
  1. It's simple.
  2. It's obvious what the code is for.
  3. I don't have to worry about edge cases.
Cons:
  1. It's ugly and verbose
  2. The try/catch block is a nontrivial* performance hit
  3. It's a statement block, so I can't make it emit an expression tree for LINQ
  4. It feels like admitting defeat

I'm not going to lie; "not admitting defeat" is the main reason I'm being so stubborn. My instincts say there must be an elegant way to do this, but finding it has been a challenge. I can't believe it's so easy to access the left side of an expression, yet the right side is nigh-unreachable.

I really have two problems here, so I'll accept anything that solves either one:

  • Expression decomposition that preserves both sides, has reasonable performance, and works on any type
  • Null-propagating member access

Update:

Null-propagating member access is planned for included in C# 6.0. I'd still like a solution to expression decomposition, though.

like image 948
Justin Morgan Avatar asked Jun 19 '12 19:06

Justin Morgan


1 Answers

If it's just a simple chain of member access expressions, there is an easy solution:

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)     where TResult : class {     return (TResult) GetValueOfExpression(target, exp.Body); }  private static object GetValueOfExpression<T>(T target, Expression exp) {     if (exp.NodeType == ExpressionType.Parameter)     {         return target;     }     else if (exp.NodeType == ExpressionType.MemberAccess)     {         var memberExpression = (MemberExpression) exp;         var parentValue = GetValueOfExpression(target, memberExpression.Expression);          if (parentValue == null)         {             return null;         }         else         {             if (memberExpression.Member is PropertyInfo)                 return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);             else                 return ((FieldInfo) memberExpression.Member).GetValue(parentValue);         }     }     else     {         throw new ArgumentException("The expression must contain only member access calls.", "exp");     } } 

EDIT

If you want to add support for method calls, use this updated method:

private static object GetValueOfExpression<T>(T target, Expression exp) {     if (exp == null)     {         return null;     }     else if (exp.NodeType == ExpressionType.Parameter)     {         return target;     }     else if (exp.NodeType == ExpressionType.Constant)     {         return ((ConstantExpression) exp).Value;     }     else if (exp.NodeType == ExpressionType.Lambda)     {         return exp;     }     else if (exp.NodeType == ExpressionType.MemberAccess)     {         var memberExpression = (MemberExpression) exp;         var parentValue = GetValueOfExpression(target, memberExpression.Expression);          if (parentValue == null)         {             return null;         }         else         {             if (memberExpression.Member is PropertyInfo)                 return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);             else                 return ((FieldInfo) memberExpression.Member).GetValue(parentValue);         }     }     else if (exp.NodeType == ExpressionType.Call)     {         var methodCallExpression = (MethodCallExpression) exp;         var parentValue = GetValueOfExpression(target, methodCallExpression.Object);          if (parentValue == null && !methodCallExpression.Method.IsStatic)         {             return null;         }         else         {             var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray();              // Required for comverting expression parameters to delegate calls             var parameters = methodCallExpression.Method.GetParameters();             for (int i = 0; i < parameters.Length; i++)             {                 if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType))                 {                     arguments[i] = ((LambdaExpression) arguments[i]).Compile();                 }             }              if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic &&                 methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method             {                 return null;             }             else             {                 return methodCallExpression.Method.Invoke(parentValue, arguments);             }         }     }     else     {         throw new ArgumentException(             string.Format("Expression type '{0}' is invalid for member invoking.", exp.NodeType));     } } 
like image 67
Balazs Tihanyi Avatar answered Oct 13 '22 07:10

Balazs Tihanyi