改变谓词的表达式树以针对另一种类型 [英] Mutating the expression tree of a predicate to target another type
问题描述
介绍
在我目前正在处理的应用程序中,每个业务对象有两种类型: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.Id
的 MemberInfo
类型的属性.在表达式树及其参数集合 (predicate.Parameters
) 中还有描述 DataContract.Widget
的 ParameterExpression
实例;所有这些都将导致错误,因为可查询主体不包含该类型的小部件,而是 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 == 0
,lambda
看起来和 referenceLambda
完全一样.但后者适用于例如IQueryable
,而前者没有;我已经在调试器的直接窗口中尝试过这个.
我还应该提到当谓词是 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屋!