Search code examples
c#lambdaexpression-trees

Expression visitor only calling VisitParameter for some lambda expressions


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


Solution

  • 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.