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.
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
?
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
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);
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.
value = x.ToMaybe() .Bind(y => y.foo) .Bind(f => f.bar) .Bind(b => b.baz) .Value;
Pros: SelectMany
and use the query syntax, IMHO that will look more messy, not less.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);
.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:result = target.Lift(x => x.foo.bar.baz)
syntaxNullable<DateTime>
members. 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: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:
Null-propagating member access is planned for included in C# 6.0. I'd still like a solution to expression decomposition, though.
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)); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With