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 這兩個我比較常用的方法了,敬請期待吧。