产品说,我只需要一个有亿点复杂的查询界面
有的時候,你需要動態構建一個比較復雜的查詢條件,傳入數據庫中進行查詢。而條件本身可能來自前端請求或者配置文件。那么這個時候,表達式樹,就可以幫助到你。本文我們將通過幾個簡短的示例來了解如何完成這些操作。
你也可能接到過這些需求
從模型進行查詢
基于配置查詢
今天我們看看表達式樹如何實現這些需求。
一切都還要從盤古開天開始說起
以下是一個簡單的單元測試用例。接下來,我們將這個測試用例改的面目全非。
[Test] public?void?Normal() {var?re?=?Enumerable.Range(0,?10).AsQueryable()?//?0-9.Where(x?=>?x?>=?1?&&?x?<?5).ToList();?//?1?2?3?4var?expectation?=?Enumerable.Range(1,?4);?//?1?2?3?4re.Should().BeEquivalentTo(expectation); }很久很久以前天和地還沒有分開
由于是 Queryable 的關系,所以Where當中的其實是一個表達式,那么我們把它單獨定義出來,順便水一下文章的長度。
[Test] public?void?Expression00() {Expression<Func<int,?bool>>?filter?=?x?=>?x?>=?1?&&?x?<?5;var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation); }有個叫盤古的巨人在這混沌之中
Expression 右側是一個 Lambda ,所以可以捕獲上下文中的變量。
這樣你便可以把 minValue 和 maxValue 單獨定義出來。
于是乎你可以從其他地方來獲取 minValue 和 maxValue 來改變 filter。
[Test] public?void?Expression01() {var?minValue?=?1;var?maxValue?=?5;Expression<Func<int,?bool>>?filter?=?x?=>?x?>=?minValue?&&?x?<?maxValue;var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation); }他睡了一萬八千年也都不用上班
那既然這樣,我們也可以使用一個方法來創建 Expression。
這個方法,實際上就可以認為是這個 Expression 的工廠方法。
[Test] public?void?Expression02() {var?filter?=?CreateFilter(1,?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateFilter(int?minValue,?int?maxValue){return?x?=>?x?>=?minValue?&&?x?<?maxValue;} }有一天盤古突然醒了但天還沒亮
那可以使用 minValue 和 maxValue 作為參數來制作工廠方法,那么用委托當然也可以。
于是,我們可以把左邊和右邊分別定義成兩個 Func,從而由外部來決定左右具體的比較方式。
[Test] public?void?Expression03() {var?filter?=?CreateFilter(x?=>?x?>=?1,?x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateFilter(Func<int,?bool>?leftFunc,?Func<int,?bool>?rightFunc){return?x?=>?leftFunc.Invoke(x)?&&?rightFunc.Invoke(x);} }他就掄起大斧頭朝前方猛劈過去
實際上,左右兩個不僅僅是兩個Func,其實也可以直接是兩個表達式。
不過稍微有點不同的是,表達式的合并需要用 Expression 類型中的相關方法創建。
我們可以發現,調用的地方這次其實沒有任何改變,因為 Lambda 既可以隱式轉換為 Func 也可以隱式轉換為 Expression。
每個方法的意思可以從注釋中看出。
[Test] public?void?Expression04() {var?filter?=?CreateFilter(x?=>?x?>=?1,?x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateFilter(Expression<Func<int,?bool>>?leftFunc,Expression<Func<int,?bool>>?rightFunc){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");//?(a?=>?leftFunc(a))(x)var?leftExp?=?Expression.Invoke(leftFunc,?pExp);//?(a?=>?rightFunc(a))(x)var?rightExp?=?Expression.Invoke(rightFunc,?pExp);//?(a?=>?leftFunc(a))(x)?&&?(a?=>?rightFunc(a))(x)var?bodyExp?=?Expression.AndAlso(leftExp,?rightExp);//?x?=>?(a?=>?leftFunc(a))(x)?&&?(a?=>?rightFunc(a))(x)var?resultExp?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);return?resultExp;} }只聽枯叉一聲黑暗漸漸地就分開
但是,上面的方法,其實可以在優化一下。避免對左右表達式的直接調用。
使用一個叫做 Unwrap 的方法,可以將 Lambda Expression 解構成只包含 Body 部分的表達式。
這是一個自定義的擴展方法,你可以通過?ObjectVisitor[1]?來引入這個方法。
限于篇幅,我們此處不能展開談 Unwrap 的實現。我們只需要關注和前一個示例中注釋的不同即可。
[Test] public?void?Expression05() {var?filter?=?CreateFilter(x?=>?x?>=?1,?x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateFilter(Expression<Func<int,?bool>>?leftFunc,Expression<Func<int,?bool>>?rightFunc){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");//?leftFunc(x)var?leftExp?=?leftFunc.Unwrap(pExp);//?rightFunc(x)var?rightExp?=?rightFunc.Unwrap(pExp);//?leftFunc(x)?&&?rightFunc(x)var?bodyExp?=?Expression.AndAlso(leftExp,?rightExp);//?x?=>?leftFunc(x)?&&?rightFunc(x)var?resultExp?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);return?resultExp;} }天和地分開后盤古怕它們還合并
我們可以再優化以下,把 CreateFilter 方法擴展為支持多個子表達式和可自定義子表達式的連接方式。
于是,我們就可以得到一個 JoinSubFilters 方法。
[Test] public?void?Expression06() {var?filter?=?JoinSubFilters(Expression.AndAlso,?x?=>?x?>=?1,?x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?JoinSubFilters(Func<Expression,?Expression,?Expression>?expJoiner,params?Expression<Func<int,?bool>>[]?subFilters){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");var?result?=?subFilters[0];foreach?(var?sub?in?subFilters[1..]){var?leftExp?=?result.Unwrap(pExp);var?rightExp?=?sub.Unwrap(pExp);var?bodyExp?=?expJoiner(leftExp,?rightExp);result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);}return?result;} }他就頭頂著天腳蹬著地不知多久
有了前面的經驗,我們知道。其實x => x >= 1這個表達式可以通過一個工廠方法來創建。
所以,我們使用一個 CreateMinValueFilter 來創建這個表達式。
[Test] public?void?Expression07() {var?filter?=?JoinSubFilters(Expression.AndAlso,CreateMinValueFilter(1),x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateMinValueFilter(int?minValue){return?x?=>?x?>=?minValue;}Expression<Func<int,?bool>>?JoinSubFilters(Func<Expression,?Expression,?Expression>?expJoiner,params?Expression<Func<int,?bool>>[]?subFilters){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");var?result?=?subFilters[0];foreach?(var?sub?in?subFilters[1..]){var?leftExp?=?result.Unwrap(pExp);var?rightExp?=?sub.Unwrap(pExp);var?bodyExp?=?expJoiner(leftExp,?rightExp);result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);}return?result;} }盤古也累得倒下來變成山石河流
當然,可以只使用 Expression 相關的方法來創建x => x >= 1。
[Test] public?void?Expression08() {var?filter?=?JoinSubFilters(Expression.AndAlso,CreateMinValueFilter(1),x?=>?x?<?5);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateMinValueFilter(int?minValue){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");//?minValuevar?rightExp?=?Expression.Constant(minValue);//?x?>=?minValuevar?bodyExp?=?Expression.GreaterThanOrEqual(pExp,?rightExp);var?result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);return?result;}Expression<Func<int,?bool>>?JoinSubFilters(Func<Expression,?Expression,?Expression>?expJoiner,params?Expression<Func<int,?bool>>[]?subFilters){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");var?result?=?subFilters[0];foreach?(var?sub?in?subFilters[1..]){var?leftExp?=?result.Unwrap(pExp);var?rightExp?=?sub.Unwrap(pExp);var?bodyExp?=?expJoiner(leftExp,?rightExp);result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);}return?result;} }那么看來盤古也吃不了上班的苦
那既然都用了 Expression 來創建子表達式了,那就干脆再做一點點改進,把x => x < 5也做成從工廠方法獲取。
[Test] public?void?Expression09() {var?filter?=?JoinSubFilters(Expression.AndAlso,CreateValueCompareFilter(Expression.GreaterThanOrEqual,?1),CreateValueCompareFilter(Expression.LessThan,?5));var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Expression<Func<int,?bool>>?CreateValueCompareFilter(Func<Expression,?Expression,?Expression>?comparerFunc,int?rightValue){var?pExp?=?Expression.Parameter(typeof(int),?"x");var?rightExp?=?Expression.Constant(rightValue);var?bodyExp?=?comparerFunc(pExp,?rightExp);var?result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);return?result;}Expression<Func<int,?bool>>?JoinSubFilters(Func<Expression,?Expression,?Expression>?expJoiner,params?Expression<Func<int,?bool>>[]?subFilters){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");var?result?=?subFilters[0];foreach?(var?sub?in?subFilters[1..]){var?leftExp?=?result.Unwrap(pExp);var?rightExp?=?sub.Unwrap(pExp);var?bodyExp?=?expJoiner(leftExp,?rightExp);result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);}return?result;} }所以如果可以不做這需求就別搞
最后,我們在把子表達式的創建通過一點點小技巧。通過外部參數來決定。就基本完成了一個多 And 的值比較查詢條件的動態構建。
[Test] public?void?Expression10() {var?config?=?new?Dictionary<string,?int>{{?">=",?1?},{?"<",?5?}};var?subFilters?=?config.Select(x?=>?CreateValueCompareFilter(MapConfig(x.Key),?x.Value)).ToArray();var?filter?=?JoinSubFilters(Expression.AndAlso,?subFilters);var?re?=?Enumerable.Range(0,?10).AsQueryable().Where(filter).ToList();var?expectation?=?Enumerable.Range(1,?4);re.Should().BeEquivalentTo(expectation);Func<Expression,?Expression,?Expression>?MapConfig(string?op){return?op?switch{">="?=>?Expression.GreaterThanOrEqual,"<"?=>?Expression.LessThan,_?=>?throw?new?ArgumentOutOfRangeException(nameof(op))};}Expression<Func<int,?bool>>?CreateValueCompareFilter(Func<Expression,?Expression,?Expression>?comparerFunc,int?rightValue){var?pExp?=?Expression.Parameter(typeof(int),?"x");var?rightExp?=?Expression.Constant(rightValue);var?bodyExp?=?comparerFunc(pExp,?rightExp);var?result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);return?result;}Expression<Func<int,?bool>>?JoinSubFilters(Func<Expression,?Expression,?Expression>?expJoiner,params?Expression<Func<int,?bool>>[]?subFilters){//?xvar?pExp?=?Expression.Parameter(typeof(int),?"x");var?result?=?subFilters[0];foreach?(var?sub?in?subFilters[1..]){var?leftExp?=?result.Unwrap(pExp);var?rightExp?=?sub.Unwrap(pExp);var?bodyExp?=?expJoiner(leftExp,?rightExp);result?=?Expression.Lambda<Func<int,?bool>>(bodyExp,?pExp);}return?result;} }還要更多
如果邏輯關系更復雜,有多層嵌套像樹形一樣,比較方法也很多花樣,甚至包含方法,怎么辦?
可以參考以下示例:
https://github.com/newbe36524/Newbe.Demo/tree/main/src/BlogDemos/Newbe.ExpressionsTests/Newbe.ExpressionsTests/FilterFactory
如果你對此內容感興趣,還可以瀏覽我之前錄制的視頻進行進一步了解:
-
戲精分享 C#表達式樹,第一季[2]
-
戲精分享 C#表達式樹,第二季[3]
你也可以參閱之前一篇入門:
《只要十步,你就可以應用表達式樹來優化動態調用》[4]
或者看MSDN文檔,我覺得你也可以有所收獲:
Expression Trees (C#) | Microsoft Docs
這篇相關的代碼,可以通過以下地址得到:
https://github.com/newbe36524/Newbe.Demo/blob/main/src/BlogDemos/Newbe.ExpressionsTests/Newbe.ExpressionsTests/Examples/Z01SingleWhereTest.cs
如果你覺得本文不錯,記得收藏、點贊、評論、轉發。告訴我還想知道點什么喲。
總結
以上是生活随笔為你收集整理的产品说,我只需要一个有亿点复杂的查询界面的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: WPF实现雷达图(仿英雄联盟)
- 下一篇: Java程序员必须知道的Java10特性