【问题标题】:Expression visitor only calling VisitParameter for some lambda expressions表达式访问者只为某些 lambda 表达式调用 VisitParameter
【发布时间】:2016-10-05 01:49:45
【问题描述】:

我希望能够使用嵌套扩展方法将 EF 中的实体投影到相应的视图模型。 (有关我在做什么的更多详细信息,请参阅我之前的问题Projection of single entities in EF with extension methods)。

根据这个问题,我构建了一个属性来用 lambda 替换表达式树中的扩展方法,以便能够做到这一点。它从扩展方法中获取方法参数,并在调用 VisitParameter 时替换它们(我不知道是否有办法替换 LambdaExpression 中的内联参数)。

这很适合这样的事情:

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

我可以看到表达式访问者将 LambdaExpression 上的实体参数替换为扩展方法 args 中的正确参数。

但是,当我将其更改为更嵌套的内容时,

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

然后我得到:

参数“entity”未绑定在指定的 LINQ to Entities 查询表达式中。

另外,VisitParameter 在我的表达式中访问者似乎根本没有被参数“实体”调用。

就像它根本不使用我的访问者来处理第二个 Lambda,但我不知道为什么它会用于一个而不是另一个?

在两种 lambda 表达式的情况下如何正确替换参数?

下面是我的访客:

    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]);
        }
    }

此处的完整重现代码示例:https://github.com/lukemcgregor/ExtensionMethodProjection

编辑:

我现在有一篇博文 (Composable Repositories - Nesting Extensions) 和 nuget package 来帮助在 linq 中嵌套扩展方法

【问题讨论】:

  • 因为ToViewModels 将拥有expandNode true,导致您返回一个将被遍历的新表达式,基本上丢弃了您的原始表达式。请记住,您将解析 ToViewModels -> AsQueryable -> SomethingElses -> 实体。
  • @rob 不确定您所说的 expandNode 是什么意思?那是什么,它是如何工作的?我可以绕过这个并继续使用这个表达式吗?
  • @downvoter 愿意评论一下您为什么认为这是一个糟糕的问题?
  • VisitMethodCall 中,您有一个基于属性的标志expandNodeToViewModels 标记有)。当expandNode 设置为true(和静态)时,它最终返回((IQueryable)node.Method.Invoke(null, args)).Expression。这是一个可能不包含原始参数表达式的新表达式。我无法运行您的完整代码(在工作中),但乍一看,这很可能是您在第二个示例中没有看到 VisitParameter 执行的原因
  • 哦,对了,对不起,我以为你在谈论一些表达式树争论。那是因为 Invoke 会创建一个全新的表达式吗?我将它包裹在 Visit() 中,现在一切正常,谢谢。

标签: c# lambda expression-trees


【解决方案1】:

首先要记住的是,在解析节点时,我们基本上是在向后运行:

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

在这里,我们处理ToViewModels(),然后是AsQueryable(),然后是SomethingElses,最后是entity。由于我们发现entity 从未被解析(VisitParameter),这意味着我们链中的某些东西停止了树的遍历。

这里有两个罪魁祸首:

VisitMethodCall()(AsQueryable 和 ToViewModels)和VisitMemberAccess()(SomethingElses)

我们没有覆盖VisitMemberAccess,所以问题必须在VisitMethodCall

该方法有三个退出点:

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

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

return base.VisitMethodCall(node);

第一行逐字返回表达式,并停止对树的进一步遍历。这意味着永远不会访问后代节点——正如我们所说的,工作基本上已经完成。这是否是正确的行为实际上取决于您希望与访问者达成的目标。

把代码改成

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

意味着我们遍历这个(可能是新的!)表达式。这并不能保证我们会访问正确的节点(例如,这个表达式可能完全独立于原始节点) - 但它确实意味着如果这个新表达式包含一个参数表达式,那么参数表达式将被正确访问。

【讨论】:

  • 非常感谢您几天来一直在努力解决这个问题的帮助:) 我真的只是在学习表达式树是如何工作的。
  • @LukeMcGregor 不是问题伙伴 :)
【解决方案2】:

我认为你过于复杂了。见访客:

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
    }

}

【讨论】:

  • OP不想用另一个方法调用(属性引用的方法)替换方法调用,他想用表达式returned替换方法调用通过属性引用的方法。
猜你喜欢
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-15
  • 1970-01-01
相关资源
最近更新 更多