使用重写规则修改 LINQ 表达式






4.98/5 (26投票s)
重写查询表达式是一种简单、安全且强大的技术,用于在运行时动态修改查询。
引言
根据用户输入在运行时动态更改 LINQ 表达式是论坛和博客中经常讨论的问题之一。我知道至少有三种解决方案,由 Tomas Petricek (http://tomasp.net/blog/linq-expand.aspx)、Joe Albahari (http://www.albahari.com/nutshell/predicatebuilder.html) 提出,并在 DynamicQuery MS 示例中实现。在本文中,我想展示如何将重写规则定义为 lambda 表达式并用于转换表达式(查询)。这项技术可以提供以下好处:
- 它适用于任何表达式的任何部分。例如,您可以动态更改 where 条件、排序或分组条件。
- 重写规则提供了转换的高级定义,不涉及表达式的任何实现细节。您不需要知道如何以及为何必须设置像“IsLifted”这样的标志来构建谓词。当然,重写引擎本身(文章附带的库中的类)在实现级别处理表达式。
重写规则本身就是 lambda 表达式,因此会受到语法和类型检查。如果您没有收到编译错误,您可以确保转换结果将是一个格式良好且类型一致的表达式。
背景
重写规则是一个有趣领域,具有扎实的数学背景。重写通常用于将表达式简化为规范形式,以检查由一组方程表示的理论所引起的它们的相等性。(例如,请参阅http://en.wikipedia.org/wiki/Knuth-Bendix_completion_algorithm。)
转换 LINQ 表达式很难被视为一个方程理论,并且不需要数学背景(:-(),无论是理解本文还是在此处描述的情况下使用重写。
演示程序
演示程序是一个 Windows Forms (VS 2008 Express) 应用程序,它从 Northwind 数据库读取 Customers 表。表单上的控件修改 where
子句条件和 orderBy
键选择器。
程序中未修改的查询如下,因此您可以看到它读取的内容:
var q = from ext in
( from c in ctx.Customers
where dummyFilter( c )
select new CustomerExt{
OrdersNo = c.Orders.Count(), Address = c.Address,
City = c.City, CompanyName = c.CompanyName,
Country = c.Country, CustomerID = c.CustomerID,
Orders = c.Orders, Region = c.Region
} )
orderby dummySelector( ext )
select ext
;
窗体上的控件
我把所有控件都放在一个窗体上,图片允许的宽度只有 600 像素,所以我们将详细检查控件以避免混淆。窗体由两个面板、一个文本框和一个网格组成。
在最上面的面板中,用户可以定义动态插入到 Where
子句中的条件。第一行中的两个控件组:
使用以下硬编码表达式:
c => SqlMethods.Like( c.City.ToUpper(), [user_input_to_upper])
和
c => c.Orders.Count( o => o.OrderDate.HasValue &&
o.OrderDate.Value.Year = [year]) >= [number])
其中方括号中的单词表示输入字段的内容。
“通用过滤器”组框中的两行定义了简单的通用二元条件。用户可以选择属性名称、比较操作并提供要比较的值。
这两个条件可以与 AND 或 OR 结合,结果可以取反。图片中的设置导致此条件:
c => c.CustomerID.StartsWith("A")
|| c.CompanyName.ToUpper().Contains("e".ToUpper())
最上面一行的前两个条件总是与通用条件通过 AND 操作连接。如果某些输入字段未填写,则相应的条件不适用。
下一个面板定义了 OrderBy
键。
前两行定义了基于属性的简单键。组合框列出了所有适用的属性。图片中显示的前两行的效果是以下查询子表达式:
.OrderBy(e => e.Region).ThenBy(y => y.City)
最后一行是为了表明任何(有效的)表达式,即使带有参数,也可以用作 OrderBy
/ThenBy
中的键选择器。此行后面的表达式是:
y => y.Orders.Count(o => o.OrderDate.HasValue
&& o.OrderDate.Value.Year = 1996)
窗体中间的文本框显示所有修改应用后的查询表达式。
查询结果显示在底部的网格中。
下载
下载内容包含一个名为 Rewrite
的库和演示应用程序。
该库定义了两个命名空间:Rewrite
和 RewriteSql
。Rewrite
命名空间包含可用于修改任何类型表达式的通用重写类。第二个命名空间 RewriteSql
使用第一个命名空间,并包含转换 LINQ to SQL 查询的 OrderBy
和 Where
子句的类。这些类不特定于演示应用程序,可以处理任何查询。
如果您想在应用程序中使用重写来修改查询的 Where
和/或 OrderBy
部分,您只需引用该库并遵循演示应用程序中的模式即可。
演示应用程序需要安装 Northwind 数据库。更改属性 DataAccess.TheConnectString
或它引用的设置。
一个失败但有助于重新发明重写规则的尝试
让我们从一个简单且相当不灵活的程序开始:
private IList<customer> GetCustomers( string city){
using( NWindDataContext ctx
= new NWindDataContext( DataAccess.TheConnectString ) ) {
var q = ctx.Customers.Where( x => x.City.StartsWith( city));
return q.ToList();
}
}
有一个参数控制选择。我们能否继续并参数化,例如,比较操作?以便我们可以传递 EndWith()
、IsEqual()
或 Like()
来替换 StartsWith()
。让我们尝试按如下方式更改程序:
private IList<Customer> GetCustomers( string city, Func<string,string,bool> comp){
using( NWindDataContext ctx
= new NWindDataContext( DataAccess.TheConnectString ) ) {
var q = ctx.Customers.Where( x => comp( x.City, city));
return q.ToList();
}
}
可以这样调用:
Func<string, string, bool> stw = ( s1, s2 ) => s1.StartsWith( s2 );
IList<Customer> lst = this.GetCustomers( "Lo", stw);
程序编译成功,但由于无法翻译查询而引发运行时异常。查询表达式为:
Table(Customer).Where(
x => Invoke(value(RewriteDemo.Form1+<>c__DisplayClass1).comp,
x.City,
value(RewriteDemo.Form1+<>c__DisplayClass1).city))
并且 comp
的方法名称是:
?comp.Method.Name
"<CallerMwthod>b__3"
程序失败不足为奇。这是在调用者中分配给 stw
的匿名委托的内部名称,尽管它只是调用 StartsWith()
,但其名称对 LINQ to Sql 查询提供程序来说是未知的。提供程序无法查看已编译的委托并了解其内容。
这提示我们可以将比已编译委托更透明的东西作为参数传递——一个表达式。这是程序的下一个版本:
private IList<Customer> GetCustomers( string city,
Expression<Func<string,string,bool>> rhs){
using( NWindDataContext ctx = new NWindDataContext(
DataAccess.TheConnectString ) ) {
Func<string, string, bool> p = null;
var q = ctx.Customers.Where( x => p( x.City, city));
Expression<Func<string, string, bool>> lhs = ( s1, s2 ) => p( s1, s2 );
Rule r = new Rule( lhs, rhs);
Expression e = Replace( q.Expression, r);
var qq = q.Provider.CreateQuery<Customer>( e);
return qq.ToList();
}
}
可以这样调用:
Expression<Func<string, string, bool>> stw =
( s1, s2 ) => s1.StartsWith( s2 );
IList<Customer> lst = this.GetCustomers( "Lo", stw);
假设的 Replace()
方法应该做什么才能让我们满意?它接受一个要转换的表达式和一对 lambda 表达式(Rule
类只是一个对)。它必须扫描目标以查找与第一个 lambda 表达式匹配的子表达式,并将其替换为第二个 lambda 表达式。就是这样。恭喜!我们重新发明了重写。
至关重要的是:Replace()
方法不需要了解 Customers、适用于字符串的特定比较操作、Where()
方法,甚至不需要了解 LINQ。它只需要知道 C# 中的表达式是如何在内部构建的以及如何操作它们。由于它封装了这些知识,我们在使用该方法时无需关心表达式的内部细节。库中没有 Replace()
方法,但静态方法 SimpleRweriter.ApplyOnce()
所做的事情与此处描述的完全相同。
替换一个委托很好,但我们需要更多来使查询灵活。当然,一个查询可以有几个“扩展点”,上面在程序中用虚拟委托 p
表示。只要这些虚拟具有不同的名称,就可以通过规则的第一个组件来区分它们,从而独立处理它们。
通过相同的机制,可以替换表达式的任何部分。属性获取器可以被替换。我们可以采用辅助表达式 p1 || p2
并替换 p1
和 p2
以获得一个合取,然后替换 Where( x => dummy( x))
中的虚拟委托以获得合取,等等。实际上,本文的主题是如何进行这种转换。
但首先,我们应该更仔细地研究重写。
重写规则
如果我们将两个 lambda 表达式以重写规则的传统形式(不幸的是,C# 3.0 也不接受 :))书写,那么上面应用的转换意图将更加明显:
p( s1, s2) --> s1.StartsWith( s2 )
这是一个“重写规则”。箭头前的表达式称为规则的左侧 (lhs);箭头后面的表达式是其右侧 (rhs)。规则的含义可以非正式地表达为:“检查您要转换的表达式的所有子表达式。如果您找到一个与规则的 lhs 匹配的子表达式,请将其替换为该规则的 rhs,用 lhs 与源表达式匹配时获得的值替换 rhs 中的变量。”
规则的左侧是在源表达式中找到的模式。右侧替换找到的子表达式。
如果规则以这种简短形式编写,它仍然缺少区分变量和检查其类型的附加信息。因此,C# 中定义规则的方式(作为两个 lambda 表达式)并没有那么冗余。必须满足一个限制:两个 lambda 表达式必须具有相同的签名,即相同数量和相同类型的参数(变量)。只有这样它们才能用作一个重写规则。参数的名称可能不同,但为了可读性,最好在两个表达式中用相同的名称表示相同的“事物”。
理论上,任何算法都可以借助重写规则来定义。在本文中,我们力求实现一个更温和的目标。与重写规则的常见使用模式相反,我们将采用一种特定的重写策略。每个规则只尝试一次,并且必须在某个子表达式处成功(在某些情况下,其中一个替代规则必须成功)。当重写规则用作通用计算引擎时,常用的策略是自下而上或自上而下地应用一组规则,只要任何规则成功即可。
使用库
在本文中,我不会描述重写引擎本身的实现。它在库外部使用的公共类是 Rule
和 SimpleRewriter
。Rule
类只是一个 lambda 表达式对。构造函数确保两个表达式都不为 null 并且具有相同的签名。静态方法 Create()
是一种构造函数,在许多情况下允许直接将 lambda 表达式作为参数写入,从而避免显式编写它们的类型。
SimpleRewriter
类的构造函数接受一个表达式。这是要转换的表达式。实例方法 ApplyOnce()
将一个规则作为参数,尝试将其应用于表达式,并在成功时返回 true。可以通过只读属性 Expression 访问结果表达式。
这是一个假设程序(抱歉,我没有真正测试过),它进行了一些众所周知的算术转换。它展示了如何使用这些类,并让您对重写规则有所了解。
public Expression Test( Expression ex){
// define the rule: (x + y) * z --> x * z + y * z
Expression<Func<int,int,int,int>> lhs1 = (x,y,z) => (x + y) * z;
Expression<Func<int,int,int,int>> rhs1 = (x,y,z) => x * z + y * z;
Rule r1 = new Rule( lhs1, rhs1);
SimpleRewriter rwr = new SimpleRewriter( ex);
while( rwr.ApplyOnce( r1))
; // apply the rule as long as it succeeds
return rwr.Expression;
}
提供论据
int a = 0;
int b = 0;
Expression<Func<int>> expr = () => (a + 3) * 1 * b;
测试程序两次应用规则并返回 a * 1 * b + 3 * 1 * b
。
可以添加其他规则
// x * 1 --> x
Expression<Func<int,int >> lhs2 = x => x * 1;
Expression<Func<int,int >> rhs2 = x => x;
Rule r2 = new Rule( lhs2, rhs2);
// x * 0 --> 0
Expression<Func<int,int >> lhs3 = x => x * 0;
// Note: the type of rhs3 is forced to be equal to the type of lhs3.
// An expression that consists
// only of a constant could have been written as () => 0.
Expression<Func<int,int >> rhs3 = x => 0;
Rule r3 = new Rule( lhs3, rhs3);
循环必须更改
while( rwr.ApplyOnce( r1)
|| rwr.ApplyOnce( r2)
|| rwr.ApplyOnce( r3)
)
;
应用于相同的源表达式,测试程序现在将返回 a * b + 3 * b
。请注意,尽管变量 b
的值为零,但规则 x*0 --> 0
不应该也不适用。转换从不考虑变量值。另请注意,方法 ApplyOnce()
不知道乘法是可交换的。规则:
// 1 * x --> x
// 0 * x --> 0
如果需要,必须明确添加。
现在应该谈谈应用规则。不是关于 ApplyOnce()
如何实现,而是关于它必须做什么。第一步是将规则的 lhs 与(子)表达式匹配。本质上,匹配类似于相等性检查,但对变量的处理方式不同。第一点,有时是混淆的来源,是,在此上下文中,只有 lhs 和 rhs 的 lambda 参数是变量。目标表达式或规则中出现的程序变量都不被视为变量。目标中 lambda 表达式的参数和规则中内部 lambda 的参数也不是变量。它们都被视为常量。匹配(如果成功)会导致替换。替换是将 variable-->expression
映射。将替换应用于表达式意味着用变量映射到的表达式替换表达式中的变量。如果存在使 lhs 与表达式字面上相等的替换,则 lhs 匹配表达式。
这是一个例子。左手边
(x + y) * z
匹配表达式
(a*1 + 3*1) * b
由于替换
x --> a*1
y --> 3*1
z --> b
如果找到了替换(匹配成功),应用规则的第二步是将替换应用于 rhs。继续示例并记住 rhs 是 x * z + y * z
,我们得到 a*1 * b + 3*1 * b
。最后一步是用这个表达式替换匹配 lhs 的表达式。
以下命题可能很有趣。如果匹配成功,则 lhs 中的所有变量都由结果替换映射。由于同一规则的 rhs 具有相同的变量,因此所有这些变量都被替换,并且变量永远不会被替换到目标表达式中。
我希望现在清楚核心重写方法必须做什么以及如何实现它。ApplyOnce()
方法及其调用的方法基于访问者模式(不足为奇),其源包含在演示项目中。
在本文的其余部分,我们将研究如何使用重写来构建和修改查询表达式。因此,我将描述库的使用和特定于查询重写的方法,当调用 ApplyOnce()
时,我将停止深入研究库。
数据访问方法
读取数据的程序如下所示。它很简单,与典型的 LINQ to SQL 数据访问方法仅通过两个额外的参数和处理这些参数的两个调用而有所不同。
public static List<CustomerExt> GetCustomersSorted(
Expression<Func<Customer, bool>> filter,
Expression<Func<IQueryable<CustomerExt>,
IOrderedQueryable<CustomerExt>>> orderByClause)
{
using( NWindDataContext ctx = new NWindDataContext( TheConnectString ) ) {
Func<Customer, bool> dummyFilter = null;
Func<CustomerExt, object> dummySelector = null;
var q = from ext in
( from c in ctx.Customers
where dummyFilter( c )
select new CustomerExt{
OrdersNo = c.Orders.Count(), Address = c.Address,
City = c.City, CompanyName = c.CompanyName,
Country = c.Country, CustomerID = c.CustomerID,
Orders = c.Orders, Region = c.Region
} )
orderby dummySelector( ext )
select ext;
var e1 = WhereRewriter.Rewrite( q.Expression, x => dummyFilter( x ), filter );
var e2 = OrderByRewriter.Rewrite( e1, x => dummySelector( x ), orderByClause );
return WhereRewriter.RecreateQuery( q, e2 ).ToList();
}
}
您肯定注意到查询中使用了两个可疑的委托:dummyFilter
和 dummySelector
,您已经知道它们的作用。您是对的——它们只是标记了必须插入动态构建的查询部分的位置。对这些虚拟的唯一要求是它们必须具有与它们出现位置相对应的类型(以便查询编译)。编译器还要求它们在使用前必须获得一个值。只要您不尝试编译/执行查询,null 值即可。查询将在 .ToList()
在返回语句中调用时执行,在此之前,我们将替换这些虚拟。
参数 filter
和 oredByClause
给出了替换虚拟的内容。它们都是 lambda 表达式。过滤 lambda 表达式的类型与 dummyFilter
委托的类型完全相同。第二个 lambda 表达式的类型基于与 dummySelector
相同的元素类型(CustomerExt
类型),但必须与查询的整个 orderby
部分的类型相对应。可以说,它包含了 orderby
关键字。类型不同的原因很明显:虽然任何过滤器都可以表示为单个布尔表达式,但排序可以通过多个键完成,因此是多个键选择器。在第一种情况下,我们使用 dummyFilter
作为标记并仅替换它。在第二种情况下,我们使用 dummySelector
作为标记并替换整个 OrderBy
子句。
例如,有效的参数值为:
c => c.City == “London” || c.City == “Lisboa”
ce => ce.OrderBy( ce.Country).ThenBy( ce.Region)
每个参数都可以为 null,分别表示“无筛选”和“无排序”。程序中使用的 WhereRewriter.RecreateQuery()
方法是一个小包装器,允许不显式编写查询类型。首先,我们将查看重写 where
和 orderby
部分的方法(分别为 WhereRewriter.Rewrite
和 OrderByRewriter.Rewrite
),然后我们将讨论重写如何帮助动态构建必须插入的查询部分。
修改 Where 子句
WhereRewriter.Rewrite()
方法如下所示(略微简化):
public static Expression Rewrite<tentity>( Expression expr, Expression<func<tentity, >> dummyPred, Expression<func<tentity, >> filter ) where TQuery: IQueryable { SimpleRewriter rwr = new SimpleRewriter( expr); if( filter == null){ Func<tentity, > p = null; // Replace the dummy predicate in the query // for the locally defined predicate "p" rwr.ApplyOnce( FilterBuilder.FilterRule( dummyPred, z => p( z ) ) ); // remove .Where( y => p(y)) from the query rwr.ApplyOnce( Rule.Create<iqueryable<tentity >,IQueryable<tentity >>( x => x.Where( y => p( y ) ), x => x ) ); } else { rwr.ApplyOnce( FilterBuilder.FilterRule( dummyPred, filter ) ); } return rwr.Expression; }
WhereRewriter
类不特定于演示,它可以修改任何对 Where()
的调用。类型参数 TEntity
必须等于目标 Where()
的 TSource
参数。
该方法接受以下三个参数:
expr
是要修改的查询表达式。它包含一个对Where()
的调用,带有一个虚拟谓词作为参数。在演示中,虚拟谓词是调用者中定义的局部变量dummyFilter
。dummyPred
是一个形如x => d(x)
的 lambda 表达式,其中d
是虚拟谓词。filter
是一个 lambda 表达式,给出要插入的条件。
如果第三个参数不为 null,则处理非常简单:第二个和第三个参数正是必须应用的规则的 lhs 和 rhs。
如果第三个参数为 null,则必须移除对 Where()
的调用。此转换可以通过以下规则完成:
// x.Where( y => d( y)) --> x
Rule r = Rule.Create<IQueryable<TEntity>,IQueryable<TEntity>>(
x => x.Where( y => dummyFilter( y)), x => x);
但是,如果这样写,规则将无法编译——变量 dummyFilter
在不同的作用域中定义。在这里,我想提醒一个典型的陷阱。看似明显的解决方案——将虚拟谓词作为附加参数传递并在规则中使用此参数——不起作用。规则将编译,但不会应用。原因是这个新参数仍然是不同的变量,它获取值但不是调用者中定义的虚拟谓词的“标识”。如果我没记错的话,在 Algol 68 中,可以将参数描述为“按值”或“按引用”,也可以描述为“按名称”。但 C# 不是 Algol 68(幸运的是 :))。
以下解决方法似乎是一个通用解决方案。我们使用一个全新的变量(程序中命名为 p
)而不是调用者中定义的变量(演示中的 dummyFilter
)来定义我们所需的所有规则。然后我们有两种可能性:
- 在所有规则中将
p
替换为dummyFilter
,或者 - 在目标表达式中将
dummyFilter
替换为p
(这在演示中完成)。
相应的规则是:
p( x) --> dummyFilter( x) // case (i)
dummyFilter( x) --> p( x) // case (ii)
这些规则也引用了超出范围的变量,但我们已经有了所需的 lambda 表达式——它作为参数 dummyPred
传递。
创建 Where 条件
现在我们将看看重写如何帮助动态创建 where
条件。通常,一个条件由原始谓词组成,这些谓词通过 AND、OR、NOT 等组合在一起。演示说明了两种创建原始谓词的方法:
- 硬编码表达式;
- 遵循通用模式生成的表达式。
硬编码谓词
这是一个硬编码原始谓词的示例:
public static Expression<Func<Customer, bool>>
MakeCityFilter( string value ) {
if( string.IsNullOrEmpty( value ) )
return null; // ---------->>>>>>>>>>>>>
value = value.ToUpper();
Expression<Func<Customer, bool>> expr =
c => SqlMethods.Like( c.City.ToUpper(), value );
// this is done solely for better readability:
expr = SimpleRewriter.ApplyOnce(
expr,
Rule.Create( () => value,
EvaluateLiteral.CreateRhs( value ) ) );
return expr;
}
没有什么可评论的。对静态方法 ApplyOnce()
的调用将其引用替换为变量 value
。这样做只是为了增强表达式的可读性:变量捕获导致难以阅读的表达式。如果变量的值为 “%a%”,那么在重写之前,表达式看起来像:
c => Like(c.City.ToUpper(),
value(RewriteDemo.UserInput+<>c__DisplayClass4).value)
重写后,它读取为:
c => Like(c.City.ToUpper(), "%A%")
硬编码谓词由编译器检查。这是一个很大的优势。硬编码谓词的缺点是它们是硬编码的(必须已经存在于源代码中)。
程序中使用的 EvaluateLiteral.CreateRhs()
方法如下所示:
public static Expression<Func<VType>> CreateRhs<VType>( VType value ) {
ConstantExpression cex = Expression.Constant( value );
return (Expression<Func<VType>>)Expression.Lambda( cex );
}
这个小函数值得更多关注。它的目的是显而易见的:例如,用参数 “LOND%”
调用,它返回 lambda 表达式 () => “LOND%”
。为了构建结果,它使用实现级别——Expression
类的类构造函数式方法。在这里我们达到了重写的极限。更多内容将在文章末尾讨论。
通用谓词
通常,原始过滤器只是将属性值与用户输入进行比较。因此,想法是准备一个以下形式的通用二元谓词(过滤器):CompareOp( entity.Prop, value)
,并用 UI 请求的特定成员替换虚拟属性和虚拟比较操作。
通用模式由 PropertyGenericBinFilter
类封装。我们来看看这个类。
public class PropertyGenericBinFilter<TEntity,TValue>:
IPropertyGenericBinFilter<TEntity> {
private Expression<Func<TEntity, bool>> _expr;
private Expression<Func<TEntity, TValue>> _propertyLhs;
private Expression<Func<TValue, TValue, bool>> _compareOpLhs;
private Expression<Func<TValue>> _valueLhs;
public PropertyGenericBinFilter() {
Func<TValue, TValue, bool> op = null;
Func<TEntity, TValue> prop = null;
TValue value = default( TValue );
_compareOpLhs = ( x, y ) => op( x, y );
_propertyLhs = e => prop( e );
_valueLhs = () => value;
_expr = e => op( prop( e ), value );
}
#region IPropertyGenericBinFilter<TEntity> Members
public Expression<Func<TEntity, bool>> Expression {
get { return _expr; }
}
public void RewriteGetter( LambdaExpression rhs ) {
_expr = SimpleRewriter.ApplyOnce( _expr,
Rule.Create( _propertyLhs,
(Expression<Func<TEntity, TValue>>)rhs ) );
}
public LambdaExpression CompareOpLhs {
get { return _compareOpLhs; }
}
public LambdaExpression ValueLhs {
get { return _valueLhs; }
}
public Type PropertyType {
get { return typeof( TValue ); }
}
#endregion
}
该类有两个类型参数——实体类型 TEntity
和属性类型 TValue
。
构造函数创建四个表达式——通用模式和三个可用作左侧替换虚拟的表达式。实例创建后,必须立即调用 RewriteGetter()
方法。它将虚拟属性获取器 prop
替换为真实的(也可以在构造函数中完成)。实例缓存到字典中。当已知的真实比较操作和要比较的值时,模式被重写并获得其最终形式,可以插入到查询中。
GenericBinFilter
类的静态方法 Create
控制从缓存创建 PropertyGenericBinFilter
实例或创建新实例所需的动作。请注意,Expression
类的静态方法用于创建替换虚拟 getter 的 rhs。GenericBinFilter
类有一个类型参数——TEntity
。Create()
方法的文本如下。
public static Expression<Func<TEntity, bool>> Create( string propertyName,
string compareOp, object value )
{
IPropertyGenericBinFilter<TEntity> propFilter;
if( !_cache.TryGetValue( propertyName, out propFilter ) ) {
PropertyInfo pi = typeof( TEntity ).GetProperty( propertyName );
if( pi == null )
return null; // ----------->>>>>>>>>>>>
Type[] genTypes = { typeof(TEntity), pi.PropertyType};
Type filterType = typeof( PropertyGenericBinFilter<,> )
.MakeGenericType( genTypes );
propFilter = (IPropertyGenericBinFilter<TEntity>)
Activator.CreateInstance( filterType );
ParameterExpression p = Expression.Parameter( typeof( TEntity ), "e" );
LambdaExpression propRhs = Expression.Lambda(
Expression.MakeMemberAccess( p, pi ),
p );
propFilter.RewriteGetter( propRhs );
_cache.Add( propertyName, propFilter );
}
SimpleRewriter rwr = new SimpleRewriter( propFilter.Expression );
// replace compare op and value
LambdaExpression opRhs = CompareOpDecoder.Decode( propFilter.PropertyType,
compareOp );
rwr.ApplyOnce( new Rule( propFilter.CompareOpLhs, opRhs ) );
rwr.ApplyOnce( new Rule( propFilter.ValueLhs,
EvaluateLiteral.CreateRhs( propFilter.PropertyType, value ) ) );
return (Expression<Func<TEntity, bool>>)rwr.Expression;
}
接口 IPropertyGenericBinFilter
定义了所有具有相同 TEntity
和不同值类型的 PropertyGenericBinFilter
对象的通用“视图”。
请注意,在这种情况下,将变量 value
替换为具有相同值的常量不仅提高了可读性。如果 where
条件包含两个(或更多)源自同一缓存实例的原始过滤器,例如:
x.City == “London” || x.City == “Lisboa”
那么两个原始过滤器实际上都会引用同一个 value
变量实例,而该实例不能同时具有“London”和“Lisboa”两个不同的值。
静态方法 CompareOpDecoder.Decode()
将字符串解码为可以插入编码操作的 lambda(如果用作规则的 rhs)。例如,如果该方法获取参数 typeof(string)
和 “StartsWith”
,它将返回以下表达式:
( x, y ) => x.StartsWith( y )
表达式从静态嵌套字典中选择,这些字典的键是类型代码和表示比较操作的字符串。这些字典对于所有实体类型(对于整个应用程序)都是通用的。下面是填充字典的一段代码。很明显,您可以根据应用程序需要用任何键/值对填充它们。
public static class CompareOpDecoder {
public static Dictionary<TypeCode, Dictionary<string, LambdaExpression>>
CompareOpDictionary;
static CompareOpDecoder() {
CompareOpDictionary =
new Dictionary<TypeCode, Dictionary<string, LambdaExpression>>();
#region string compare
Dictionary<string, LambdaExpression> stringOps
= new Dictionary<string, LambdaExpression>
( StringComparer.InvariantCultureIgnoreCase );
stringOps.AddStringPredicate( "==", ( x, y ) => x == y );
stringOps.AddStringPredicate( "!=", ( x, y ) => x != y );
stringOps.AddStringPredicate( "StartsWith", ( x, y ) => x.StartsWith( y ) );
stringOps.AddStringPredicate( "EndsWith", ( x, y ) => x.EndsWith( y ) );
stringOps.AddStringPredicate( "Contains", ( x, y ) => x.Contains( y ) );
stringOps.AddStringPredicate( "IStartsWith",
( x, y ) => x.ToUpper().StartsWith( y.ToUpper() ) );
stringOps.AddStringPredicate( "IEndsWith",
( x, y ) => x.ToUpper().EndsWith( y.ToUpper() ) );
stringOps.AddStringPredicate( "IContains",
( x, y ) => x.ToUpper().Contains( y.ToUpper() ) );
stringOps.AddStringPredicate( "Like", ( x, y ) => SqlMethods.Like( x, y ) );
stringOps.AddStringPredicate( "ILike",
( x, y ) => SqlMethods.Like( x.ToUpper(),
y.ToUpper() ) );
CompareOpDictionary.Add( TypeCode.String, stringOps );
#endregion
函数 AddStringPredicate()
是一个小型辅助函数。它规定了参数的类型,因此不需要显式声明每个表达式的类型。
private static void AddStringPredicate(
this Dictionary<string, LambdaExpression> dict,
string opName,
Expression<Func<string, string, bool>> expr )
{
dict.Add( opName, expr );
}
如果我们需要动态构建谓词,上面用于说明硬编码谓词的 MakeCityFilter()
方法可以按以下方式编写:
public static Expression<Func<Customer, bool>> MakeCityFilter( string value ) {
if( string.IsNullOrEmpty( value ) )
return null; // ---------->>>>>>>>>>>>>
return GenericBinFilter<Customer>.Create( “City”, “ILike”, value );
}
组合谓词
FilterBuilder
类的静态方法借助布尔运算将谓词组合成更复杂的谓词。我们来看看 And()
方法。
public static Expression<Func<TEntity, bool>> And<TEntity>(
params Expression<Func<TEntity, bool>>[] args ) {
Func<TEntity, bool> p1 = null;
Func<TEntity, bool> p2 = null;
Expression<Func<TEntity, bool>> expr = e => p1( e);
SimpleRewriter rwr = new SimpleRewriter( expr );
bool noArgs = true;
foreach( var arg in args.Where( y => y != null ) ) {
noArgs = false;
rwr.ApplyOnce( FilterRule<TEntity>( e => p1(e), e => p2(e) && p1(e)));
rwr.ApplyOnce( FilterRule( e => p2( e), arg));
}
if( noArgs )
return null; // ----------->>>>>>>>>>>>>>>>>>>>
// remove p1
Expression<Func<bool, TEntity, bool>> lhs = ( x, e ) => x && p1( e );
Expression<Func<bool, TEntity, bool>> rhs = ( x, e ) => x;
rwr.ApplyOnce( new Rule( lhs, rhs ) );
return (Expression<Func<TEntity, bool>>)rwr.Expression;
}
该方法接受一系列谓词,并返回所有非空谓词的合取,如果不存在则返回 null。结果表达式最初只包含 p1
。对于每个非空参数,p1
被替换为 p2 && p1
,紧接着,p2
被替换为当前参数。
这是重写步骤的追踪(假设所有参数都不为 null):
e => p1( e)
e => p2( e) && p1( e)
e => arg1( e) && p1( e)
e => arg1( e) && (p2( e) && p1( e))
e => arg1( e) && (arg2( e) && p1( e))
e => arg1( e) && (arg2( e) && (p2( e) && p1( e)))
e => arg1( e) && (arg2( e) && (arg3( e) && p1( e)))
. . .
最后,如果存在非空参数,表达式将如下所示:
arg1 && (arg2 && (arg3 ... && ( argN && p1) ... ))
规则 x && p1 --> x
从表达式中删除 p1
,有效地产生所需的结果。
修改 OrderBy 部分
OrderByRewriter.Rewrite()
方法与 WhereRewriter.Rewrite()
非常相似。它将对 OrderBy()
的调用替换为虚拟委托,以其第三个参数的表达式体。更有趣的是查看这个表达式是如何构建的。
创建 OrderBy 表达式
这里的核心类是 OrderByItem
。该类的一个实例保存一个键选择器表达式和一个升序/降序标志。它的属性 OrderByExpr
和 ThenByExpr
返回基于保存信息的表达式。OrderByExpr
属性的文本如下:
public Expression<Func<IQueryable<TEntity>,IOrderedQueryable<TEntity>>>
OrderByExpr {
get {
Func<TEntity, TKey> dummy = null;
Expression<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>> ret;
if( _ascending )
ret = x => x.OrderBy( y => dummy( y));
else
ret = x => x.OrderByDescending( y => dummy( y));
return SimpleRewriter.ApplyOnce( ret,
Rule.Create( y => dummy( y ), _keySelector ) );
}
}
该属性很简单,几乎不需要注释。函数 OrderBy
/ThenBy
有两个参数——一个要排序的序列和一个定义序列项顺序的键选择器委托。由于柯里化,属性 OrderByExpr
和 ThenByExpr
返回只有一个参数的表达式,因为保存的委托已经嵌入其中。
OrderByItem
类有两个构造函数。第一个接受键选择器表达式和布尔值,用于创建基于硬编码选择器键的 OrderBy
/ThenBy
调用。第二个接受 PropertyInfo
和布尔值,并创建基于通用选择器键表达式的 OrderBy
/ThenBy
调用。
硬编码选择器键
当在编译时已知键选择器时,只需几行代码即可创建 OrderByItem
实例。任意复杂的带参数表达式都可以用作选择器。以下是演示应用程序中的一段代码:
int year;
if( int.TryParse( this.cboSortYear.Text, out year ) ) {
Expression<Func<CustomerExt, int>> expr3 =
z => z.Orders.Count( o => o.OrderDate.HasValue
&& o.OrderDate.Value.Year == year );
// for better readability:
expr3 = SimpleRewriter.ApplyOnce( expr3,
Rewrite.Rule.Create( () => year,
EvaluateLiteral.CreateRhs( year ) ) );
skey3 = new OrderByItem<CustomerExt, int>(
expr3,
true );
}
与上面的硬编码过滤器一样,这里的重写仅用于提高可读性。我希望您会同意,"z => z.Orders.Count(o => (o.OrderDate.HasValue && (o.OrderDate.Value.Year = 1997)))"
比 "z => z.Orders.Count(o => (o.OrderDate.HasValue && (o.OrderDate.Value.Year = value(RewriteDemo.Form1+<>c__DisplayClass2).year)))"
更易读。
通用选择器键
大多数实际使用的键选择器只是排序项的属性。这些简单的选择器由接受 PropertyInfo
作为第一个参数的构造函数构建。构造函数使用 Expression
类的静态方法。其文本如下:
public OrderByItem( PropertyInfo pi, bool ascending ) {
_ascending = ascending;
ParameterExpression p = Expression.Parameter( typeof( TEntity ), "x" );
_keySelector = (Expression<Func<TEntity, TKey>>)Expression.Lambda(
Expression.MakeMemberAccess( p, pi ),
p );
}
OrderByItem
类有两个类型参数:序列元素的类型和键选择器返回的类型(这些正是 OrderBy()
函数的类型参数)。第一个在编译时已知。当可以请求任意属性时,无法静态调用适当的构造函数(第二个类型在编译时未知)。BuildGeneric()
方法动态调用所需的构造函数:
public static IOrderByItem<TEntity>BuildGeneric(
string propName, bool ascending )
{
PropertyInfo pi = typeof( TEntity ).GetProperty( propName );
if( pi == null )
throw new ArgumentOutOfRangeException(
"propName",
string.Format( "Property '{0}' not found in type '{1}'.",
propName, typeof( TEntity ).Name ) );
Type[] genTypes = { typeof( TEntity ), pi.PropertyType };
Type type = typeof( OrderByItem<,> ).MakeGenericType( genTypes );
return (IOrderByItem<TEntity>)
Activator.CreateInstance( type, pi, ascending );
}
构建 OrderBy 子句
OrderByBuilder.MakeOrderByClause()
方法与 FilterBuilder.And()
非常相似,但它使用函数应用而不是 AND 连接表达式。下面是文本和一些注释。
public static Expression<Func<IQueryable<TEntity>,IOrderedQueryable<TEntity>>>
MakeOrderByClause( params IOrderByItem<TEntity>[] args )
{
bool firstItem = true;
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> oby = null;
Func<IOrderedQueryable<TEntity>, IOrderedQueryable<TEntity>> thby1 = null;
Func<IOrderedQueryable<TEntity>, IOrderedQueryable<TEntity>> thby2 = null;
Expression<Func<IOrderedQueryable<TEntity>, IOrderedQueryable<TEntity>>>
lhs1 = x => thby1( x );
Expression<Func<IOrderedQueryable<TEntity>, IOrderedQueryable<TEntity>>>
rhs1 = x => thby1( thby2( x ) );
Expression<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>>
ret = x => thby1( oby( x ) );
SimpleRewriter rwr = new SimpleRewriter( ret );
foreach( var arg in args.Where( a => a != null ) ) {
if( firstItem ) {
firstItem = false;
// replace oby --> arg.OrderByExpr
rwr.ApplyOnce( Rule.Create( x => oby( x ), arg.OrderByExpr ) );
} else {
// replace thby1 --> thby2
rwr.ApplyOnce( new Rule( lhs1, rhs1 ) );
// replace thby2 --> arg.ThenByExpr
rwr.ApplyOnce( Rule.Create( x => thby2( x ), arg.ThenByExpr ) );
}
}
if( firstItem )
return null; // -------------->>>>>>>>>>>>>>>>>>>>>>
// remove thby1: replace thby1( x) --> x
rwr.ApplyOnce( Rule.Create( lhs1, x => x ) );
return (Expression<Func<IQueryable<TEntity>,
IOrderedQueryable<TEntity>>>)
rwr.Expression;
}
该方法将一系列 IOrderByItem
对象作为参数(实际上是类型为 OrderByItem
且第二个位置具有不同类型参数的对象——没有其他类型实现 IOrderByItem
)。
最后要返回的表达式被初始化为 x => thby1( oby( x ) )
。第一个非空参数的 OrderByExpr
属性中的函数替换了虚拟委托 oby
。因此表达式变为 x => thby1( f1( x ) )
,其中 f1
是由 arg[ 0].OrderByExpr
返回的 lambda 表达式的主体。正如您所记得的,由于 OrderBy
/OrderByDescending
的柯里化,主体只有一个参数。
对于每个下一个非空参数,虚拟委托 thby1
被替换为 thby1( thby2())
。表达式变为 x => thby1( thby2( f1( x ) ) )
。然后,thby2
被替换为 arg[ i].ThenByExpr
的主体,产生 x => thby1( f2( f1( x ) ) )
。最后,表达式看起来像 x => thby1( fN( ... f2( f1( x )) ... ))
。对 thby1
的调用被移除,结果正是我们需要的。
硬编码与动态生成表达式
您可能已经注意到,重写可以重新排列表达式树中的节点,它可以插入规则右侧中已存在的节点,但它不能创建新节点。结果由源表达式中已存在的节点和已应用规则右侧的节点组成。如果我们知道转换结果必须包含,例如,函数 StartsWith()
,整数上的谓词 >=
,某个子查询,或者属性的 getter,那么这些元素必须已经存在于源表达式的某个位置,或者,更可能地,在规则的 rhs 中。因此,无论我们的规则多么简单或智能,我们都需要某种规则库存(或池),其中包含所有必需的“构建块”。这很好地匹配了商业应用程序中的典型场景:用户可以选择此或彼现有过滤器并输入参数,但不能“发明”新的过滤器、新的排序标准等。
库存中的所有规则都在编译时已知,并由编译器检查。这是一个很大的优势。如果属性名称或类型发生更改,编译器将找到所有您忘记修剪的规则。
如果不可能或不合理地为所有“构建块”硬编码规则(适度懒惰不是罪过),则可以基于通用模式动态创建附加规则。规则本身是(成对的)表达式,因此可以重写通用模式以生成特定规则。该模式给出了所需表达式的通用结构,并且只需要通过调用 Expression
类的类构造函数静态方法来创建单个节点(更精确地说:只有具有一个节点和其主体中包含变量的简单 lambda 表达式)。这种方法安全性较低,仅适用于遵循某些通用模式的表达式。
最后的 remarks
本文旨在介绍重写查询表达式的基础知识。库中的类当然不完美,但它们可以完成工作。它们可以变得更智能以涵盖更多情况。例如,如果它们在应用程序中有意义,可以使用不同的 Where
条件和 OrderBy
选择器模式。为了说明这项技术,我只是选择了一些“经典”案例。
修改 GroupBy
部分在技术上也不应该复杂。
当然,我会尽力回答任何问题。