简介
在我目前正在处理的应用程序中,每个业务对象有两种:“ActiveRecord”类和“DataContract”类 . 例如,会有:
namespace ActiveRecord {
class Widget {
public int Id { get; set; }
}
}
namespace DataContract {
class Widget {
public int Id { get; set; }
}
}
数据库访问层负责在系列之间进行转换:您可以告诉它更新 DataContract.Widget
,它会神奇地创建一个具有相同属性值的 ActiveRecord.Widget
并保存它 .
尝试重构此数据库访问层时,问题浮出水面 .
问题
我想在数据库访问层中添加如下方法:
// 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
类型的属性,用于描述 DataContract.Widget.Id
. 表达式树及其参数集合( predicate.Parameters
)中也有 ParameterExpression
个实例,它们描述了 DataContract.Widget
;所有这些都会导致错误,因为可查询主体不包含该类型的小部件,而是 ActiveRecord.Widget
.
搜索了一下之后,我发现了System.Linq.Expressions.ExpressionVisitor(它的源代码可以在how-to的上下文中找到here),这提供了一种修改表达式树的便捷方法 . 在.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
时,一切正常 . 因此,我假设我没有找到更多的线索 .
最终解决方案
考虑到问题的正确答案(下面两个;一个简短,一个带代码)问题解决了;我把代码和一些重要的注释放在separate answer中,以防止这个长问题变得更长 .
感谢大家的回答和评论!
6 回答
事实证明,棘手的部分只是新lambda的表达式树中存在的
ParameterExpression
实例必须 the same instances ,因为在Expression.Lambda
的IEnumerable<ParameterExpression>
参数中传递 .请注意,在
TransformPredicateLambda
中我给t => typeof(TNewTarget)
作为"type converter"函数;那是因为在这种特定情况下,我们可以假设所有参数和成员访问都属于那种特定类型 . 更高级的场景可能需要额外的逻辑 .代码:
上面的Jon's own answer很棒,所以我将它扩展为处理方法调用,常量表达式等,以便现在它也适用于以下表达式:
我也取消了
ExpressionTreeExplorer
,因为我们唯一需要的是ParameterExpressions .这是代码(更新:完成转换后清除缓存)
我认为如果你正确地进行查询,Linq-To-Sql将生成所需的SQL . 在这种情况下,使用
IQueryable
和延迟执行可以避免返回所有ActiveRecord.Widget
记录 .ExecuteTypedList不能完成你想做的事吗? SubSonic将填充您的DTO / POCO . 来自Rob Connery的博客:
这里's the link to Rob' s Writing Decoupled, Testable code with SubSonic 2.1
看来你在这里的VisitMember()中生成了两次参数表达式:
...因为base.Visit()最终会出现在我想象的VisitParameter中,并且在GetMany()本身中:
如果您在正文中使用ParameterExpression,则必须如此与为Lambda声明的实例相同的实例(不仅仅是相同的类型和名称) . 我之前遇到过这种情况的问题,虽然我认为结果是我只是无法创建表达式,它只会引发异常 . 在任何情况下,您可能会尝试重用参数实例,看看它是否有帮助 .
我尝试了简单(不完整)的实现来改变表达式
p => p.Id == 15
(代码如下) . 有一个名为"CrossMapping"的类定义了原始类型和"new"类型与类型成员之间的映射 .对于每种表达式类型,有几个名为
Mutate_XY_Expression
的方法,它们会产生新的变异表达式 . 方法输入需要原始表达式(MemberExpression originalExpression
)作为表达式的模型,列表或参数表达式(IList<ParameterExpression> parameterExpressions
)是"parent"表达式定义的参数,应该由"parent's" body使用,映射对象(CrossMapping mapping
)定义之间的映射类型和成员 .对于完整实现,您可能需要父项表达式而不是参数的更多信息 . 但模式应该是相同的 .
正如您所知,示例并未实现访问者模式 - 这是因为简单性 . 但转换到它们没有障碍 .
我希望,这会有所帮助 .
代码(C#4.0):