Create an Expression for another with different ar

2019-06-08 15:34发布

问题:

This question already has an answer here:

  • Convert Linq expression “obj => obj.Prop” into “parent => parent.obj.Prop” 1 answer

Is it possible to create an Expression that receives a type X from an Expression that receives a type Y if X contains Y inside it?

For example, I have these types:

public class Y {
    public int Something { get; set; }
}

public class X {
    public Y Y { get; set; }
}

Let's say I have this function that returns an Y Expression:

private static Expression<Func<Y, bool>> ExprY(Y y2)
{
    return y1 => y1.Something == y2.Something;
}

As you can see, I can send a Y object to this function and it will return an Expression<Func<Y, bool>>.

Now, let's say that I want to create the same Expression but from X, that way I can write this:

private static Expression<Func<X, bool>> ExprX(X x2)
{
    return x1 => x1.Y.Something == x2.Y.Something;
}

This works fine, but I am duplicating code since the same logic to compare Something is inside both functions, so the question is how to rewrite ExprX to somehow use ExprY inside itself so I don't need to repeat the Something comparison?

Something like that:

public static Expression<Func<X, bool>> ExprX(X x2)
{
    return x1 => ExprY(x2.Y)(x1.Y);
}

PS. this is intended to be code run by Entity Framework 6 in a .Where clause, so the answer needs to work with that framework.

回答1:

You can use ExpressionVisitor to replace all Y in the expression with x.Y and build the new expression.

class ReplaceParameterExpressionVisitor : ExpressionVisitor
{
    private readonly Expression _target, _replacement;

    public ReplaceParameterExpressionVisitor(ParameterExpression target, Expression replacement)
    {
        _target = target;
        _replacement = replacement;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node.Equals(_target) ? _replacement : node;
    }
}

The visitor is very simple, find the target ParameterExpression and replace it with the _replacement expression.

public static class ExpressionExtensions
{
    public static Expression<Func<T, R>> ReplaceParameter<T, R, U>(this Expression<Func<U, R>> origin, Expression<Func<T, U>> accessor)
    {
        var originalParameter = origin.Parameters.Single();
        var replacement = accessor.Body;
        var visitor = new ReplaceParameterExpressionVisitor(originalParameter, replacement);
        var newBody = visitor.Visit(origin.Body);
        return Expression.Lambda<Func<T, R>>(newBody, accessor.Parameters.Single());
    }
}

The above extension method will replace the parameters in the body using the visitor, get the new body, and build a new lambda expression with the new parameter.

class User
{
    public string Name { get; set; }
}

Expression<Func<string, bool>> exp = s => s.Equals("Jack");
Expression<Func<User, string>> nameAccessor = u => u.Name;
var newExp = exp.ReplaceParameter(nameAccessor);   // u => u.Name.Equals("Jack")