4

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

Community
  • 1
  • 1
undefined
  • 33,537
  • 22
  • 129
  • 198
  • Because `ToViewModels` will have `expandNode` true, causing you to return a new expression which will be traversed, essentially discarding you r original expression. Remember that you'll be parsing ToViewModels -> AsQueryable -> SomethingElses -> entity. – Rob Oct 05 '16 at 03:08
  • @rob not sure what you mean about expandNode true? What is that and how does it work? Can I get around this and keep using this expression? – undefined Oct 05 '16 at 03:37
  • @downvoter care to comment on why you feel this is a poor question? – undefined Oct 05 '16 at 03:38
  • In `VisitMethodCall`, you've got a flag `expandNode` based on the attributes (which `ToViewModels` is marked with). When `expandNode` is set to true (and static), it eventually returns `((IQueryable)node.Method.Invoke(null, args)).Expression`. This is a new expression which likely does not contain the original parameter expression. I'm not able to run your full code (at work), but at a glance, it's very likely this is the cause of you not seeing `VisitParameter` execute on the second example – Rob Oct 05 '16 at 03:41
  • Oh right sorry i thought you were talking about some expression tree arguement. Is that because the Invoke will create a brand new expression? I wrapped it in a `Visit()` and now everything works, thanks. – undefined Oct 05 '16 at 03:52
  • @rob If you put that as an answer ill mark it :) – undefined Oct 05 '16 at 03:53
  • Just for my understanding calling Visit will make the current visitor process that expression rather than creating a new visitor to do it? – undefined Oct 05 '16 at 03:54
  • Without Visit() you're essentially stopping the traversal of the tree. The method becomes a 'leaf' node (even though the result may be a tree itself). Wrapping it in Visit allows your visitor code to also execute on the new expression – Rob Oct 05 '16 at 04:12
  • I think there is a big confusion here. Your visitor does NOT need any provider reference. You do not care abou the provider, should NEVER call provider createquery, and usually invoke is also a smell. – MBoros Oct 06 '16 at 15:04

2 Answers2

2

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.

Rob
  • 26,989
  • 16
  • 82
  • 98
0

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
    }

}
MBoros
  • 1,090
  • 7
  • 19
  • The OP doesn't want to replace the method call with another method call (the method that the attribute references), he wants to replace the method call with the expression **returned** by the method referenced by the attribute. – Arad Alvand Oct 09 '20 at 08:58