简介
在我当前正在开发的应用程序中,每个业务对象都有两种类型:“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;为了有效地做到这一点(想想 LINQ to SQL),我需要传递一个表达式树,所以这个方法只需要这个。
障碍:参数需要从 Expression>
神奇地转换为 Expression>.
尝试的解决方案
我想在 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
实例,其具有描述 DataContract.Widget.Id
的 MemberInfo
类型的属性。
表达式树及其参数集合 (predicate.Parameters
) 中还有描述 DataContract.Widget
的 ParameterExpression
实例;所有这些都会导致错误,因为可查询主体不包含该类型的小部件,而是包含 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.Where
一起使用,而前者则不能;我已经在调试器的直接窗口中尝试过此操作。
我还应该提到当谓词是 w =>; true
,一切正常。因此,我假设我在访问者中没有做足够的工作,但我找不到更多的线索可以跟踪。
最终解决方案
考虑到问题的正确答案(下面两个;一个简短,一个带有代码),问题就解决了;我将代码和一些重要注释放在单独的答案中,以防止这个长问题变得更长。
感谢大家的回答和评论!
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!
发布评论
评论(6)
看来您在 VisitMember() 中生成了两次参数表达式:
...因为我想象 base.Visit() 将在 VisitParameter 中结束,并且在 GetMany() 本身中:
如果您在body,它必须与为 Lambda 声明的实例是相同的实例(而不仅仅是相同的类型和名称)。
我以前遇到过这种情况的问题,尽管我认为结果是我无法创建表达式,它只会抛出异常。在任何情况下,您都可以尝试重用参数实例,看看是否有帮助。
It seems you're generating the parameter expression twice, in VisitMember() here:
...since base.Visit() will end up in VisitParameter I imagine, and in GetMany() itself:
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.
事实证明,棘手的部分很简单,新 lambda 表达式树中存在的
ParameterExpression
实例必须与中传递的实例相同
参数。Expression.Lambda
的 IEnumerable请注意,在
TransformPredicateLambda
中,我给出了t =>; typeof(TNewTarget)
作为“类型转换器”函数;这是因为在这种特定情况下,我们可以假设所有参数和成员访问都属于该特定类型。更高级的场景可能需要额外的逻辑。代码:
It turned out that the tricky part is simply that the
ParameterExpression
instances that exist in the expression tree of the new lambda must be the same instances as are passed in theIEnumerable<ParameterExpression>
parameter ofExpression.Lambda
.Note that inside
TransformPredicateLambda
I am givingt => typeof(TNewTarget)
as the "type converter" function; that's because in this specific case, we can assume that all parameters and member accesses will be of that one specific type. More advanced scenarios may need additional logic in there.The code:
我尝试了简单(不完整)的实现来改变表达式
p =>; p.Id == 15
(代码如下)。有一个名为“CrossMapping”的类,它定义原始类型和“新”类型以及类型成员之间的映射。对于每种表达式类型,都有几种名为
Mutate_XY_Expression
的方法,这会生成新的变异表达式。方法输入需要原始表达式(MemberExpression OriginalExpression
)作为表达式模型,列表或参数表达式(IListparameterExpressions
),它们是“parent”定义的参数表达式并应由“父级”主体以及定义类型和成员之间映射的映射对象(CrossMapping 映射
)使用。为了完全实现,您可能需要来自父级表达式的比参数更多的信息。但模式应该是一样的。
如您所知,示例没有实现访问者模式 - 这是因为简单。但皈依它们并不存在障碍。
我希望,它会有所帮助。
代码(C#4.0):
I tried the simple (not complete) implementation for mutating the expression
p => p.Id == 15
(the code is below). There are one class named "CrossMapping" which defines the mapping between original and "new" types and type members.There are several metods named
Mutate_XY_Expression
for every expression type, which makes new mutated expression. The method inputs need the original express (MemberExpression originalExpression
) as model of expression, the list or parameters expression (IList<ParameterExpression> parameterExpressions
) which are defined parameters by "parent" expression and should be used by "parent's" body, and the mapping object (CrossMapping mapping
) which defines the mapping between types and members.For full implementation you will maybe need more informations from parent's expression than parameters. But the pattern should be same.
Sample does not implement the Visitor pattern, as you know - it's because simplicity. But there is no barrier to converting to them.
I hope, it will help.
The code (C# 4.0):
上面的 Jon 自己的答案非常棒,所以我将其扩展为处理方法调用、常量表达式等,这样现在它就可以工作了也适用于诸如以下的表达式:
我还取消了
ExpressionTreeExplorer
因为我们唯一需要的是 ParameterExpressions。这是代码(更新:完成转换后清除缓存)
Jon's own answer above is great, so I expanded it to handle method calls, constant expressions, etc. so that now it works also for expressions such as:
I also did away with the
ExpressionTreeExplorer
since the only thing we need are the ParameterExpressions.Here's the code (Update: Clear the cache when done converting)
ExecuteTypedList 没有完成您想要做的事情吗? SubSonic 将填充您的 DTO/POCO。来自罗布·康纳利的博客:
这是 Rob 的 使用 SubSonic 2.1 编写解耦、可测试的代码
Doesn't ExecuteTypedList accomplish what you want to do? SubSonic will populate your DTO's/POCOs. From Rob Connery's blog:
Here's the link to Rob's Writing Decoupled, Testable code with SubSonic 2.1
我认为如果您正确执行查询,Linq-To-Sql 将生成所需的 SQL。在这种情况下,使用
IQueryable
和延迟执行可以避免返回所有ActiveRecord.Widget
记录。I think Linq-To-Sql will produce the desirable SQL if you do your queries correctly. In this case, using
IQueryable
and deferred execution you can avoid returning allActiveRecord.Widget
records.