1. 程式人生 > >[C#.NET 拾遺補漏]13:動態構建LINQ查詢表示式

[C#.NET 拾遺補漏]13:動態構建LINQ查詢表示式

最近工作中遇到一個這樣的需求:在某個列表查詢功能中,可以選擇某個數字列(如商品單價、當天銷售額、當月銷售額等),再選擇 `小於或等於` 和 `大於或等於` ,再填寫一個待比較的數值,對資料進行查詢過濾。 如果只有一兩個這樣的數字列,那麼使用 Entity Framework Core 可以這麼寫 LINQ 查詢: ```cs public Task> GetProductsAsync(string propertyToFilter, MathOperator mathOperator, decimal value) { var query = _context.Products.AsNoTracking(); query = propertyToFilter switch { "Amount1" when mathOperator == MathOperator.LessThanOrEqual => query.Where(x => x.Amount1 <= value), "Amount1" when mathOperator == MathOperator.GreaterThanOrEqual => query.Where(x => x.Amount1 >= value), "Amount2" when mathOperator == MathOperator.LessThanOrEqual => query.Where(x => x.Amount2 <= value), "Amount2" when mathOperator == MathOperator.GreaterThanOrEqual => query.Where(x => x.Amount2 >= value), _ => throw new ArgumentException($"不支援 {propertyToFilter} 列作為數字列查詢", nameof(propertyToFilter)) }; return query.ToListAsync(); } ``` 如果固定只有一兩個數字列且將來也不會再擴充套件,這樣寫簡單粗暴,也沒什麼問題。 但如果有幾十個數字列,這樣使用 `swith` 模式匹配的寫法就太恐怖了,程式碼大量重複。很自然地,我們得想辦法根據屬性名動態建立 `Where` 方法的引數。它的引數型別是:`Expression>`,是一個表示式引數。 要知道如何動態建立一個類似 `Expression>` 型別的表示式例項,就要知道如何拆解表示式樹。 對於本示例,以 `x => x.Amount1 <= value` 表示式例項為例,它的表示式樹是這樣的: ![](https://w-share.oss-cn-shanghai.aliyuncs.com/20201125015109.png) 然後我們可以按照此表示式樹結構來構建我們的 LINQ 表示式: ```cs public Task> GetProductsAsyncV2(string propertyToFilter, MathOperator mathOperator, decimal value) { var query = _context.Products.AsNoTracking(); var paramExp = Expression.Parameter(typeof(Product)); var memberExp = Expression.PropertyOrField(paramExp, propertyToFilter); var valueExp = Expression.Constant(value); var compareExp = mathOperator == MathOperator.LessThanOrEqual ? Expression.LessThanOrEqual(memberExp, valueExp) : Expression.GreaterThanOrEqual(memberExp, valueExp); var lambda = Expression.Lambda>(compareExp, paramExp); return query.Where(lambda).ToListAsync(); } ``` 每個 `Expression.XXX` 靜態方法返回的都是一個以 `Expression` 為基類的例項,代表一個表示式。不同的表示式又可以組成一個新的表示式,直到得到我們需要的 Lambda 表示式。這樣就形成了一種樹形結構,我們稱為**表示式樹**。知道如何把一個最終的查詢表示式拆解成表示式樹,我們就容易動態構建此查詢表示式。 得到一個表示式後,我們還可以動態編譯並呼叫該表示式,比如上面示例得到的 `lambda` 變數,是一個`Expression>` 型別,呼叫其 `Compile` 方法,可以得到 `Func` 型別的委託。 ```cs ... var toTestProduct = new Product { Amount1 = 100, Amount2 = 200 }; Func func = lambda.Compile(); var result = func(toTestProduct); Console.WriteLine($"The product's {propertyToFilter} is to {mathOperator} {value}."); // Output: The product's Amount1 is LessThanOrEqual to 150. ``` 你可以通過研究 `Expression` 類來了解更多動態構建表示式的方法。 動態構建 LINQ 表示式對於不能在編譯時建立查詢,只能在執行時建立查詢的場景很有用。但它的缺點也很明顯,不易維護、不易閱讀、不易除錯。如果最終的表示式執行出錯,很難通過除錯來發現具體是構建中的那一步寫錯了,只能憑自己的理解和經驗查詢錯誤。所以,如非必須,一般不推薦動態構建 LINQ 查詢表