C# 動態構建表達式樹(一)—— 構建 Where 的 Lambda 表達式


C# 動態構建表達式樹(一)—— 構建 Where 的 Lambda 表達式

前言

記得之前同事在做篩選功能的時候提出過一個問題:如果用戶傳入的條件數量不確定,條件的內容也不確定(大於、小於和等於),能否能夠動態拼接成 Linq 后在數據庫篩選,當時也沒有好的思路。最近看的教程上提到了“動態構建表達式樹”,剛好可以解決此類問題。

准備工作

環境:.NET Framework 4.5,SQLServer 2017

建表腳本如下(由 SSMS 導出):

USE [default]
GO
/****** Object:  Table [dbo].[Person]    Script Date: 2021/6/9 12:06:43 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Person](
	[Id] [varchar](100) NOT NULL,
	[Name] [nvarchar](50) NOT NULL,
	[Age] [int] NOT NULL,
	[Gender] [nvarchar](5) NOT NULL,
	[Point] [int] NOT NULL,
	[CreateTime] [datetime] NOT NULL,
 CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

表中數據如下:

動態構建“屬性值”比較的表達式

要查詢的為滿足:Gender 為“男”的,且 Point 小於 10000的數據

按照常規寫法:

List<Person> personList = context.Person.Where(p => p.Gender == "男" && p.Point < 10000).ToList();

動態組合的寫法:

ParameterExpression pe = Expression.Parameter(typeof(Person), "p");	# 創建形參 p

MemberExpression meGender = Expression.Property(pe, "Gender");	# 獲取 p 的屬性 Gender
BinaryExpression beGenderCondition = Expression.Equal(meGender, Expression.Constant("男")); # 比較

MemberExpression mePoint = Expression.Property(pe, "Point");	# 獲取 p 的屬性 Point
BinaryExpression bePointCondition = Expression.LessThan(mePoint, Expression.Constant(10000)); # 比較

BinaryExpression resultCondition = Expression.AndAlso(bePointCondition, beGenderCondition);	# 組合兩個條件

Expression<Func<Person, bool>> personFilterExpression = 
	Expression.Lambda<Func<Person, bool>>(resultCondition, pe);	# 創建最終 lambda 表達式
List<Person> personList1 = context.Person.Where(personFilterExpression).ToList();	# 執行查詢

從上面的代碼中可以看出,Expression 類包含了所有有可能的操作。所謂動態組合,就是使用 Expression 類的各種方法,改寫原始寫法,最終組合形成表達式。如:獲取屬性時我們使用的“.”(點號),可以通過 Expression.Property 方法來實現,“小於”操作符可以通過 Expression.LessThan 方法來實現。

動態構建“屬性方法”比較的表達式

要查詢的為滿足:Gender 是以 “男” 開頭的數據(別問為什么有這么奇怪的需求,我一時想不到好的例子了XD)

按照常規寫法:

List<Person> personList = context.Person.Where(p => p.Gender.StartsWith("男")).ToList();

動態組合的寫法:

ParameterExpression pe = Expression.Parameter(typeof(Person), "p");	# 創建形參 p
MemberExpression meGender = Expression.Property(pe, "Gender");	# 獲取 p 的屬性 Gender
MethodCallExpression mceGender = 
	Expression.Call(meGender, "StartsWith", null, Expression.Constant("男"));	# 調用 StartsWith 方法

Expression<Func<Person, bool>> personFilterExpression 
	= Expression.Lambda<Func<Person, bool>>(mceGender, pe);	# 創建最終 lambda 表達式
List<Person> personList1 = context.Person.Where(personFilterExpression).ToList(); # 執行查詢

注意,調用方法時,也需要使用 Expression.Property 獲取屬性,再參與操作!

LINQ to Entities 中不能識別的方法(如 DateTime 類型的 ToString 方法)依然不能通過這種方式調用!

全表查詢的問題

先說結論:向 IQueryable 類型傳入 Expression 類型,會通過數據庫查詢出符合條件的內容並返回;向 IQueryable 類型傳入 Func 類型,會查出全表,在程序中過濾后返回。(可以通過 ChangeTracer 中的內容或 SQL 執行情況判斷是否全表查詢)

一種表達式原則上應該只有一種類型才對,但這一點似乎對 lambda 表達式不適用。

# 寫法1 數據庫全表查詢
Func<Person, bool> func = p => p.Point < 10000 && p.Gender == "男";
List<Person> funcPersonList = context.Person.Where(func).ToList();

#寫法2 數據庫按需查詢
Expression<Func<Person, bool>> expression = p => p.Point < 10000 && p.Gender == "男";
List<Person> expressionPersonList = context.Person.Where(expression).ToList();

這兩種寫法均不會報錯。但注意觀察,“寫法1” 中的 context.Person 類型已經變為了 IEnumerable,而正常應該是 IQueryable

至於原因其實也很簡單,因為 IQueryable 繼承自 IEnumerable,IQueryable.Where 只支持 Expression 作為參數,而 IEnumerable 只支持 Func 作為參數

如果要將Expression 轉換為 Func,可以調用 Expression.Compile 方法。

后記

最近在聽朝夕教育的體驗課,本文的主要的內容也是其中講動態表達式的內容。其中有個聽課的同學提出,傳入參數類型為 Func 和 Expression 會有不同的效果,這也給了我很大啟發(准確地說是幫我避開了一個大坑),在這里表示感謝。

因為能力有限,一口實在吃不下太多,因此本文寫的主要是向 Where 方法傳遞 lambda 表達式參數,也算是一個入門了。在后面的內容打算涉及 Select 和 Group 這兩個我比較常用的方法了,敬請期待吧。

參考

動態構建Expression表達式樹

Func轉Expression的方法(C#)

動態生成C# Lambda表達式

Linq To EF 用泛型時生成的Sql會查詢全表的問題

Expression<Func >和Func


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM