上一篇文章中我们利用C#语言的特性实现了一种轻量级的Specification模式,它的关键在于抛弃了具体的Specification类型,而是使用一个委托对象代替唯一关键的IsSatisfiedBy方法逻辑。据我们分析,其优势之一在于使用简单,其劣势之一在于无法静态表示。但是它们还都是在处理“业务逻辑”,如果涉及到一个用于LINQ查询或其他地方的表达式树,则问题就不那么简单了——但也没有我们想象的那么复杂。
好,那么我们就把场景假想至LINQ上。LINQ与普通业务逻辑不同的地方在于,它不是用一个IsSatisfiedBy方法或一个委托对象用来表示判断逻辑,而是需要构造一个表达式树,一种数据结构。如果您还不清楚表达式树是什么,那么可以看一下脑袋的写的上手指南。这是.NET 3.5带来的重要概念,在4.0中又得到了重要发展,如果您要在.NET方面前进,这是一条必经之路。
And、Or和Not之间,最容易处理的便是Not方法,于是我们从这个地方下手,直接来看它的实现:
public static class SpecExprExtensions
{
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> one)
{
var candidateExpr = one.Parameters[0];
var body = Expression.Not(one.Body);
return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}
}
一个Expression<TDelegate>对象中主要有两部分内容,一是参数,二是表达式体(Body)。对于Not方法来说,我们只要获取它的参数表达式,再将它的Body外包一个Not表达式,便可以此构造一个新的表达式了。这部分逻辑非常简单,看了脑袋的文章,了解了表达式树的基本结构就能理解这里的含义。那么试验一下:
Expression<Func<int, bool>> f = i => i % 2 == 0;
f = f.Not();
foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }.AsQueryable().Where(f))
{
Console.WriteLine(i);
}
打印出来的自然是所有的奇数,即1、3、5。
而And和Or的处理上会有所麻烦,我们不能这样像Not一样简单处理:
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another)
{
var candidateExpr = one.Parameters[0];
var body = Expression.And(one.Body, another.Body);
return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}
这么做虽然能够编译通过,但是在执行时便会出错。原因在于one和another两个表达式虽然都是同样的形式(Expression<Func<T, bool>>),但是它们的“参数”不是同一个对象。也就是说,one.Body和another.Body并没有公用一个 ParameterExpression实例,于是我们无论采用哪个表达式的参数,在Expression.Lambda方法调用的时候,都会告诉您新的 body中的某个参数对象并没有出现在参数列表中。
于是,我们如果要实现And和Or,做的第一件事情便是统一两个表达式树的参数,于是我们准备一个ExpressionVisitor:
internal class ParameterReplacer : ExpressionVisitor
{
public ParameterReplacer(ParameterExpression paramExpr)
{
this.ParameterExpression = paramExpr;
}
public ParameterExpression ParameterExpression { get; private set; }
public Expression Replace(Expression expr)
{
return this.Visit(expr);
}
protected override Expression VisitParameter(ParameterExpression p)
{
return this.ParameterExpression;
}
}