表达式访问者仅对某些lambda表达式调用VisitParameter [英] Expression visitor only calling VisitParameter for some lambda expressions

查看:55
本文介绍了表达式访问者仅对某些lambda表达式调用VisitParameter的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望能够使用嵌套扩展方法将EF中的实体投影到相应的视图模型. (请参阅我以前的问题使用扩展方法在EF中对单个实体的投影了解即时消息的详细信息.

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

这对于类似这样的东西很有效:

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

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

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

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

然后我得到:

在指定的LINQ to Entities查询表达式中未绑定参数"entity".

另外,在表达式访问者中,VisitParameter似乎根本没有通过参数'entity'被调用.

这就像第二个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]);
        }
    }

此处提供完整的repro代码示例: https://github.com/lukemcgregor/ExtensionMethodProjection

我现在有一篇博客文章(可组合的存储库-嵌套扩展)和 nuget包来帮助在linq中嵌套扩展方法

解决方案

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

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

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

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

解决方案

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.

这篇关于表达式访问者仅对某些lambda表达式调用VisitParameter的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆