Expression visitor only calling VisitParameter for some lambda expressions

c# expression-trees lambda

Question

I want to be able to used nested extension methods to do projection of entities in EF to corresponding view models. (see my previous question Projection of single entities in EF with extension methods for more details on what im doing).

As per this question I built an attribute to replace an extension method in an expression tree with a lambda to be able to do this. It takes the method arguments from the extentsion method and replaces them on as VisitParameter is called (I dont know if there is a way to replace the parameters inline in the LambdaExpression).

This works well for something like this:

entity => new ProfileModel
{
    Name = entity.Name  
}

And I can see the expression visitor replace the entity parameter on the LambdaExpression to the correct one from the extension method args.

However when I change it to something more nested say,

entity => new ProfileModel
{
    SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}

then I get:

The parameter 'entity' was not bound in the specified LINQ to Entities query expression.

Additionally VisitParameter in my expression visitor doesn't seem to get called at all with the parameter 'entity'.

Its like its not using my visitor at all for the second Lambda, but I dont know why It would for one and not the other?

How can I correctly replace the parameter in the case of both types of lambda expressions?

My Visitor below:

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        bool expandNode = node.Method.GetCustomAttributes(typeof(ExpandableMethodAttribute), false).Any();
        if (expandNode && node.Method.IsStatic)
        {
            object[] args = new object[node.Arguments.Count];
            args[0] = _provider.CreateQuery(node.Arguments[0]);

            for (int i = 1; i < node.Arguments.Count; i++)
            {
                Expression arg = node.Arguments[i];
                args[i] = (arg.NodeType == ExpressionType.Constant) ? ((ConstantExpression)arg).Value : arg;
            }
            return ((IQueryable)node.Method.Invoke(null, args)).Expression;
        }
        var replaceNodeAttributes = node.Method.GetCustomAttributes(typeof(ReplaceInExpressionTree), false).Cast<ReplaceInExpressionTree>();
        if (replaceNodeAttributes.Any() && node.Method.IsStatic)
        {
            var replaceWith = node.Method.DeclaringType.GetMethod(replaceNodeAttributes.First().MethodName).Invoke(null, null);
            if (replaceWith is LambdaExpression)
            {
                RegisterReplacementParameters(node.Arguments.ToArray(), replaceWith as LambdaExpression);
                return Visit((replaceWith as LambdaExpression).Body);
            }
        }
        return base.VisitMethodCall(node);
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        Expression replacement;
        if (_replacements.TryGetValue(node, out replacement))
            return Visit(replacement);
        return base.VisitParameter(node);
    }
    private void RegisterReplacementParameters(Expression[] parameterValues, LambdaExpression expressionToVisit)
    {
        if (parameterValues.Length != expressionToVisit.Parameters.Count)
            throw new ArgumentException(string.Format("The parameter values count ({0}) does not match the expression parameter count ({1})", parameterValues.Length, expressionToVisit.Parameters.Count));
        foreach (var x in expressionToVisit.Parameters.Select((p, idx) => new { Index = idx, Parameter = p }))
        {
            if (_replacements.ContainsKey(x.Parameter))
            {
                throw new Exception("Parameter already registered, this shouldn't happen.");
            }
            _replacements.Add(x.Parameter, parameterValues[x.Index]);
        }
    }

Full repro code example here: https://github.com/lukemcgregor/ExtensionMethodProjection

Edit:

I now have a blog post (Composable Repositories - Nesting Extensions) and nuget package to help with nesting extension methods in linq

Accepted Answer

First thing to remember is that when parsing nodes, we essentially run backwards:

entity => new ProfileModel
{
    SomethingElses = entity.SomethingElses.AsQueryable().ToViewModels()
}

Here, we process ToViewModels(), then AsQueryable(), then SomethingElses, and finally entity. Since we're finding that entity is never parsed (VisitParameter), it means something in our chain stopped the traversal of the tree.

We have two culprits here:

VisitMethodCall() (AsQueryable and ToViewModels) and VisitMemberAccess() (SomethingElses)

We're not overriding VisitMemberAccess, so the problem must lie within VisitMethodCall

We have three exit points for that method:

return ((IQueryable)node.Method.Invoke(null, args)).Expression;

return Visit((replaceWith as LambdaExpression).Body);

return base.VisitMethodCall(node);

The first line returns an expression verbatim, and stops further traversal of the tree. This means descendant nodes will never be visited - as we're saying the work is essentially done. Whether or not this is correct behavior really depends on what you're wanting to achieve with the visitor.

Changing the code to

return Visit(((IQueryable)node.Method.Invoke(null, args)).Expression);

Means we traverse this (potentially new!) expression. This doesn't guarantee we'll visit the correct nodes (for example, this expression may be completely independent of the original) - but it does mean that if this new expression contained a parameter expression, that the parameter expression would be visited properly.


Popular Answer

I think you overcomplicated it. See the visitor:

public class CustomerVM { }
public class Customer {}

public class ReplaceMethodAttribute: Attribute
{
    public string ReplacementMethodName {get; private set;}
    public ReplaceMethodAttribute(string name)
    {
        ReplacementMethodName = name;
    }
}

public static class Extensions
{
    public static CustomerVM ToCustomerVM(Customer customer)
    {
        throw new NotImplementedException();
    }
    [ReplaceMethod("Extensions.ToCustomerVM")]
    public static CustomerVM ToVM(this Customer customer)
    {
        return Extensions.ToCustomerVM(customer);
    }
}

public class ReplaceMethodVisitor: ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression exp)
    {
        var attr = exp.Method.GetCustomAttributes(typeof(ReplaceMethodAttribute), true).OfType<ReplaceMethodAttribute>().FirstOrDefault();
        if (attr != null)
        {
            var parameterTypes = exp.Method.GetParameters().Select(i => i.ParameterType).ToArray();
            var mi = GetMethodInfo(attr.ReplacementMethodName, parameterTypes);
            return Visit(Expression.Call(mi, exp.Arguments));
        }
        return base.VisitMethodCall(exp);
    }

    private MethodInfo GetMethodInfo(string name, Type[] argumentTypes)
    {
        // enhance with input checking
        var lastDot = name.LastIndexOf('.');
        var type = name.Substring(0, lastDot);
        var methodName = name.Substring(lastDot);
        return this.GetType().Assembly.GetTypes().Single(x => x.FullName == type).GetMethod(methodName, argumentTypes); // this might need adjusting if types are in different assembly
    }

}


Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why