表达式访问者只为一些lambda表达式调用VisitParameter

时间:2016-10-05 01:49:45

标签: c# lambda expression-trees

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

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

这适用于这样的事情:

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

我可以看到表达式visitor将LambdaExpression上的entity参数替换为扩展方法args中的正确参数。

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

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

然后我得到:

  

参数'实体'未绑定在指定的LINQ to Entities查询表达式中。

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

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

修改

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

2 个答案:

答案 0 :(得分: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);

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

答案 1 :(得分:0)

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

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
    }

}