10

I have an existing expression of type Expression<Func<T, object>>; it contains values like cust => cust.Name.

I also have a parent class with a field of type T. I need a method that accepts the above as a parameter and generates a new expression that takes the parent class (TModel) as a parameter. This will be used as an expression parameter of an MVC method.

Thus, cust => cust.Name becomes parent => parent.Customer.Name.

Likewise, cust => cust.Address.State becomes parent => parent.Customer.Address.State.

Here's my initial version:

    //note: the FieldDefinition object contains the first expression
    //described above, plus the MemberInfo object for the property/field
    //in question
    public Expression<Func<TModel, object>> ExpressionFromField<TModel>(FieldDefinition<T> field)
        where TModel: BaseModel<T>
    {
        var param = Expression.Parameter(typeof(TModel), "t");

        //Note in the next line "nameof(SelectedItem)". This is a reference
        //to the property in TModel that contains the instance from which
        //to retrieve the value. It is unqualified because this method
        //resides within TModel.
        var body = Expression.PropertyOrField(param, nameof(SelectedItem));
        var member = Expression.MakeMemberAccess(body, field.Member);
        return Expression.Lambda<Func<TModel, object>>(member, param);
    }

The error I'm currently receiving is when I have a field with multiple parts (i.e. cust.Address.State instead of just cust.Name). I get an error on the var member line that the specified member doesn't exist--which is true, since the body at that refers to the parent's child (Customer) and not the item that contains the member (Address).

Here's what I wish I could do:

    public Expression<Func<TModel, object>> ExpressionFromField<TModel>(FieldDefinition<T> field)
        where TModel: BaseModel<T>
    {
        var param = Expression.Parameter(typeof(TModel), "t");
        var body = Expression.PropertyOrField(param, nameof(SelectedItem));
        var IWantThis = Expression.ApplyExpressionToField(field.Expression, body);
        return Expression.Lambda<Func<TModel, object>>(IWantThis, param);
    }

Any help getting to this point would be greatly appreciated.

Edit: This was marked as a possible duplicate of this question; however, the only real similarity is the solution (which is, in fact, the same). Composing expressions is not an intuitive solution to accessing nested properties via expressions (unless one's inuition is guided by certain experience, which should not be assumed). I also edited the question to note that the solution needs to be suitable for a paramter of an MVC method, which limits the possible solutions.

Community
  • 1
  • 1
Daniel
  • 1,695
  • 15
  • 33
  • Why is it not an option to call these expressions from the children properties of the parent instead of making them mutate to work directly on the parent? – Travis J Jun 02 '16 at 21:56
  • Using `parent => (cust => cust.Name)(parent.Customer)` would be much simpler. – poke Jun 02 '16 at 22:06
  • @TravisJ, I'm using all this to wire up base classes, so few of the specifics are known upfront. There may be a better way to do what I'm doing, but this way will work. – Daniel Jun 02 '16 at 22:29
  • @poke, I'm not sure what that does. Are you casting a customer as an expression? And can you do the same thing with unknown expressions (I don't know the type of `parent`, `cust`, or the field or type of `Name` upfront. – Daniel Jun 02 '16 at 22:30
  • That's pseudo-code: Essentially, keep the function that gives you the property from the object; and then use that function in a new expression that takes the parent and calls the function with the parent's child. Basically classic function composition instead of complex expression rewriting. – poke Jun 02 '16 at 22:34
  • @poke Gotcha. The resulting expression is used as a parameter to an MVC method, so my options are limited. – Daniel Jun 02 '16 at 22:36
  • 1
    Possible duplicate of [How do I compose Linq Expressions? ie Func>, Exp>, Exp>>](http://stackoverflow.com/questions/2330843/how-do-i-compose-linq-expressions-ie-funcexpfuncx-y-expfuncy-z-exp) – felipe Dec 13 '16 at 14:58

1 Answers1

15

What you're looking for is the ability to compose expressions, just as you can compose functions:

public static Expression<Func<T, TResult>> Compose<T, TIntermediate, TResult>(
    this Expression<Func<T, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    return Expression.Lambda<Func<T, TResult>>(
        second.Body.Replace(second.Parameters[0], first.Body),
        first.Parameters[0]);
}

This relies on the following method to replace all instances of one expression with another:

public class ReplaceVisitor:ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression ex)
    {
        if(ex == from) return to;
        else return base.Visit(ex);
    }  
}

public static Expression Replace(this Expression ex,
    Expression from,
    Expression to)
{
    return new ReplaceVisitor(from, to).Visit(ex);
}

You can now take an expression selecting a property:

Expression<Func<Customer, object>> propertySelector = cust => cust.Name;

And an expression selecting that object from the model:

Expression<Func<CustomerModel, Customer>> modelSelector = model => model.Customer;

and compose them:

Expression<Func<Customer, object> magic = modelSelector.Compose(propertySelector);
Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
Servy
  • 202,030
  • 26
  • 332
  • 449
  • 3
    Holy crap that is SO COOL. I was able to quickly implement this, but I'll want to spend some more time with it to figure out exactly how it works. I also made a couple quick edits to your answer; Intermediate was misspelled once and once of the parameter types was missing. – Daniel Jun 02 '16 at 22:25
  • Youll want to `return to` in your visit method as well – D. Ben Knoble Jun 03 '16 at 01:29