改变谓词的表达式树以针对另一种类型 [英] Mutating the expression tree of a predicate to target another type

查看:22
本文介绍了改变谓词的表达式树以针对另一种类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

介绍

在我目前正在处理的应用程序中,每个业务对象有两种类型:ActiveRecord"类型和DataContract"类型.例如,会有:

命名空间 ActiveRecord {类小部件{公共 int Id { 获取;放;}}}命名空间数据契约{类小部件{公共 int Id { 获取;放;}}}

数据库访问层负责家族之间的转换:你可以告诉它更新一个DataContract.Widget,它会神奇地创建一个ActiveRecord.Widget属性值并保存.

尝试重构此数据库访问层时出现问题.

问题

我想在数据库访问层添加如下方法:

//Widget 为 DataContract.Widget接口 IDbAccessLayer {IEnumerable<小部件>GetMany(Expression<Func<Widget, bool>>谓词);}

以上是带有自定义谓词的简单通用get"方法.唯一的兴趣点是我传递的是表达式树而不是 lambda,因为在 IDbAccessLayer 内部我正在查询 IQueryable;为了有效地做到这一点(想想 LINQ to SQL)我需要传入一个表达式树,所以这个方法只需要这个.

问题:参数需要从 Expression 神奇地转换为 Expression;.

尝试的解决方案

我想在 GetMany 中做的是:

IEnumerable得到许多(表达式<Func<DataContract.Widget, bool>>谓词){var lambda = Expression.Lambda(predicate.Body,谓词.参数);//使用 lambda 查询 ActiveRecord.Widget 并返回一些值}

这行不通,因为在典型情况下,例如如果:

谓词 == w =>w.Id == 0;

...表达式树包含一个 MemberAccessExpression 实例,该实例具有描述 DataContract.Widget.IdMemberInfo 类型的属性.在表达式树及其参数集合 (predicate.Parameters) 中还有描述 DataContract.WidgetParameterExpression 实例;所有这些都将导致错误,因为可查询主体不包含该类型的小部件,而是 ActiveRecord.Widget.

搜索了一下,我找到了 System.Linq.Expressions.ExpressionVisitor(其来源可在 此处 在操作说明的上下文中),它提供了一种修改表达式树的便捷方法.在 .NET 4 中,这个类是开箱即用的.

有了这个,我实现了一个访问者.这个简单的访问者只负责更改成员访问和参数表达式中的类型,但这足以使用谓词 w =>w.Id == 0.

内部类访问者:ExpressionVisitor{private readonly Func类型转换器;公共访问者(Func typeConverter){this.typeConverter = typeConverter;}受保护的覆盖表达式访问成员(MemberExpression 节点){var dataContractType = node.Member.ReflectedType;var activeRecordType = this.typeConverter(dataContractType);var 转换 = Expression.MakeMemberAccess(base.Visit(node.Expression),activeRecordType.GetProperty(node.Member.Name));回报转换;}受保护的覆盖表达式访问参数(ParameterExpression 节点){var dataContractType = node.Type;var activeRecordType = this.typeConverter(dataContractType);return Expression.Parameter(activeRecordType, node.Name);}}

有了这个访问者,GetMany 变成:

IEnumerable得到许多(表达式<Func<DataContract.Widget, bool>>谓词){var 访客 = 新访客(...);var lambda = Expression.Lambda(访客.访问(谓词.正文),predicate.Parameters.Select(p =>visitor.Visit(p));var widgets = ActiveRecord.Widget.Repository().Where(lambda);//仅供参考,见下文表达式<Func<ActiveRecord.Widget, bool>>参考Lambda =w=>w.Id == 0;//在这里,我们将小部件转换为 DataContract.Widget 的实例,然后//返回它们——不过这与问题无关.}

结果

好消息是 lambda 构造得很好.坏消息是它不起作用.当我尝试使用它时,它对我很不利,而且异常消息真的一点帮助都没有.

我检查了我的代码生成的 lambda 和具有相同表达式的硬编码 lambda;它们看起来完全一样.我花了几个小时在调试器中试图找到一些差异,但我不能.

当谓词是 w =>;w.Id == 0lambda 看起来和 referenceLambda 完全一样.但后者适用于例如IQueryable.Where,而前者没有;我已经在调试器的直接窗口中尝试过这个.

我还应该提到当谓词是 w =>;真的,一切正常.因此,我假设我没有在访问者身上做足够的工作,但我找不到更多的线索.

最终解决方案

考虑到问题的正确答案(下面有两个;一个简短,一个有代码)问题解决了;我将代码和一些重要的注释放在一个单独的答案中,以免这个冗长的问题变得更长.>

感谢大家的回答和评论!

解决方案

您似乎在 VisitMember() 中生成了两次参数表达式:

var 转换 = Expression.MakeMemberAccess(base.Visit(node.Expression),activeRecordType.GetProperty(node.Member.Name));

...因为 base.Visit() 将在我想象的 VisitParameter 中结束,并且在 GetMany() 本身中:

var lambda = Expression.Lambda>(访客.访问(谓词.正文),predicate.Parameters.Select(p =>visitor.Visit(p));

如果您在主体中使用 ParameterExpression,它必须与为 Lambda 声明的实例相同(不仅仅是相同的类型和名称).我以前遇到过这种情况的问题,但我认为结果是我无法创建表达式,它只会抛出异常.在任何情况下,您都可以尝试重用参数实例,看看它是否有帮助.

Intro

In the application I 'm currently working on, there are two kinds of each business object: the "ActiveRecord" kind and the "DataContract" kind. So for example, there would be:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

The database access layer takes care of translating between families: you can tell it to update a DataContract.Widget and it will magically create an ActiveRecord.Widget with the same property values and save that instead.

The problem surfaced when attempting to refactor this database access layer.

The Problem

I want to add methods like the following to the database access layer:

// Widget is DataContract.Widget

interface IDbAccessLayer {
    IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}

The above is a simple general-use "get" method with custom predicate. The only point of interest is that I am passing in an expression tree instead of a lambda because inside IDbAccessLayer I am querying an IQueryable<ActiveRecord.Widget>; to do that efficiently (think LINQ to SQL) I need to pass in an expression tree so this method asks for just that.

The snag: the parameter needs to be magically transformed from an Expression<Func<DataContract.Widget, bool>> to an Expression<Func<ActiveRecord.Widget, bool>>.

Attempted Solution

What I 'd like to do inside GetMany is:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        predicate.Body,
        predicate.Parameters);

    // use lambda to query ActiveRecord.Widget and return some value
}

This won't work because in a typical scenario, for example if:

predicate == w => w.Id == 0;

...the expression tree contains a MemberAccessExpression instance which has a property of type MemberInfo that describes DataContract.Widget.Id. There are also ParameterExpression instances both in the expression tree and in its parameter collection (predicate.Parameters) that describe DataContract.Widget; all of this will result in errors since the queryable body does not contain that type of widget but rather ActiveRecord.Widget.

After searching a bit, I found System.Linq.Expressions.ExpressionVisitor (its source can be found here in the context of a how-to), which offers a convenient way to modify an expression tree. In .NET 4, this class is included out of the box.

Armed with this, I implemented a visitor. This simple visitor only takes care of changing the types in member access and parameter expressions, but that's enough functionality to work with the predicate w => w.Id == 0.

internal class Visitor : ExpressionVisitor
{
    private readonly Func<Type, Type> typeConverter;

    public Visitor(Func<Type, Type> typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var dataContractType = node.Member.ReflectedType;
        var activeRecordType = this.typeConverter(dataContractType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            activeRecordType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var dataContractType = node.Type;
        var activeRecordType = this.typeConverter(dataContractType);

        return Expression.Parameter(activeRecordType, node.Name);
    }
}

With this visitor, GetMany becomes:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var visitor = new Visitor(...);
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        visitor.Visit(predicate.Body),
        predicate.Parameters.Select(p => visitor.Visit(p));

    var widgets = ActiveRecord.Widget.Repository().Where(lambda);

    // This is just for reference, see below
    Expression<Func<ActiveRecord.Widget, bool>> referenceLambda = 
        w => w.Id == 0;

    // Here we 'd convert the widgets to instances of DataContract.Widget and
    // return them -- this has nothing to do with the question though.
}

Results

The good news is that lambda is constructed just fine. The bad news is that it isn't working; it's blowing up on me when I try to use it, and the exception messages are really not helpful at all.

I have examined the lambda my code produces and a hardcoded lambda with the same expression; they look exactly the same. I spent hours in the debugger trying to find some difference, but I can't.

When the predicate is w => w.Id == 0, lambda looks exactly like referenceLambda. But the latter works with e.g. IQueryable<T>.Where, while the former does not; I have tried this in the immediate window of the debugger.

I should also mention that when the predicate is w => true, everything works fine. Therefore I am assuming that I 'm not doing enough work in the visitor, but I can't find any more leads to follow.

Final Solution

After taking into account the correct answers to the problem (two of them below; one short, one with code) the problem was solved; I put the code along with a few important notes in a separate answer to keep this long question from becoming even longer.

Thanks to everyone for your answers and comments!

解决方案

It seems you're generating the parameter expression twice, in VisitMember() here:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

...since base.Visit() will end up in VisitParameter I imagine, and in GetMany() itself:

var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
    visitor.Visit(predicate.Body),
    predicate.Parameters.Select(p => visitor.Visit(p));

If you're using a ParameterExpression in the body, it has to be the same instance (not just the same type and name) as the one declared for the Lambda. I've had problems before with this kind of scenario, though I think the result was that I just wasn't able to create the expression, it would just throw an exception. In any case you might try reusing the parameter instance see if it helps.

这篇关于改变谓词的表达式树以针对另一种类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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