将谓词的表达式树突变为目标另一种类型
介绍
在我当前正在处理的应用程序中,每种业务对象都有两种:“ ActiveRecord”类型和“ DataContract”类型。因此,例如,将有:
namespace ActiveRecord {
class Widget {
public int Id { get; set; }
}
}
namespace DataContract {
class Widget {
public int Id { get; set; }
}
}
数据库访问层负责在族之间进行转换:您可以告诉它更新a
DataContract.Widget
,它将神奇地创建一个ActiveRecord.Widget
具有相同属性值的an 并将其保存。
尝试重构此数据库访问层时出现了问题。
问题
我想将以下方法添加到数据库访问层:
// Widget is DataContract.Widget
interface IDbAccessLayer {
IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}
上面是带有自定义谓词的简单的通用“
get”方法。唯一感兴趣的一点是,我传递的是表达式树而不是lambda,因为在内部IDbAccessLayer
我正在查询IQueryable<ActiveRecord.Widget>
;
为了有效地做到这一点(想想LINQ to SQL),我需要传递一个表达式树,这样该方法就可以做到这一点。
障碍:需要将参数从神奇地转换Expression<Func<DataContract.Widget,
bool>>
为Expression<Func<ActiveRecord.Widget, bool>>
。
尝试的解决方案
我想在里面做的GetMany
是:
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
}
这将不起作用,因为在典型情况下,例如:
predicate == w => w.Id == 0;
…表达式树包含一个MemberAccessExpression
实例,该实例的属性类型MemberInfo
为describe
DataContract.Widget.Id
。ParameterExpression
在表达式树及其参数集合(predicate.Parameters
)中也有实例描述DataContract.Widget
;
所有这些都会导致错误,因为可查询主体不包含该类型的小部件,而是ActiveRecord.Widget
。
搜索了一下后,我发现System.Linq.Expressions.ExpressionVisitor
(其来源可以发现这里的如何做的情况下),它提供了一个方便的方式来修改表达式树。在.NET
4中,该类是开箱即用的。
有了这个,我实现了一个访客。这个简单的访问者只需要更改成员访问和参数表达式中的类型,但这足以使用谓词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);
}
}
通过此访客,GetMany
将成为:
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.
}
结果
好消息是,lambda
它的构造很好。坏消息是它不起作用。当我尝试使用它时,它就爆炸了,异常消息真的根本没有帮助。
我检查了我的代码产生的lambda和具有相同表达式的硬编码lambda。他们看起来完全一样。我在调试器中花了几个小时试图找到一些区别,但是我做不到。
当谓词为时w => w.Id ==
0
,lambda
看起来完全像referenceLambda
。但是后者适用于例如IQueryable<T>.Where
,而前者则不适用;我已经在调试器的即时窗口中尝试过此操作。
我还应该提到,当谓词为时w => true
,一切正常。因此,我假设我没有在访问者中做足够的工作,但是我找不到更多的线索可循。
最终解决方案
在考虑了问题的正确答案之后(以下两个);一个简短的问题,一个带有代码的问题得以解决;我将代码和一些重要说明放在一个单独的答案中,以防止这个漫长的问题变得更长。
感谢大家的回答和评论!
-
似乎您在VisitMember()中两次生成了参数表达式:
var converted = Expression.MakeMemberAccess( base.Visit(node.Expression), activeRecordType.GetProperty(node.Member.Name));
…因为base.Visit()将以我想象的VisitParameter和GetMany()本身结尾:
var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>( visitor.Visit(predicate.Body), predicate.Parameters.Select(p => visitor.Visit(p));
如果您在主体中使用ParameterExpression,则它必须与为Lambda声明的实例具有相同的实例(不仅仅是相同的类型和名称)。在这种情况下,我曾经遇到过问题,尽管我认为结果是我无法创建表达式,但它只会引发异常。无论如何,您都可以尝试重用参数实例,以查看是否有帮助。