產品說,我只需要一個有億點複雜的查詢介面
- 2021 年 10 月 25 日
- 筆記
有的時候,你需要動態構建一個比較複雜的查詢條件,傳入資料庫中進行查詢。而條件本身可能來自前端請求或者配置文件。那麼這個時候,表達式樹,就可以幫助到你。本文我們將通過幾個簡短的示例來了解如何完成這些操作。
你也可能接到過這些需求
今天我們看看錶達式樹如何實現這些需求。
一切都還要從盤古開天開始說起
以下是一個簡單的單元測試用例。接下來,我們將這個測試用例改的面目全非。
[Test]
public void Normal()
{
var re = Enumerable.Range(0, 10).AsQueryable() // 0-9
.Where(x => x >= 1 && x < 5).ToList(); // 1 2 3 4
var expectation = Enumerable.Range(1, 4); // 1 2 3 4
re.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)
{
// x
var 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)
{
// x
var 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)
{
// x
var 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)
{
// x
var 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)
{
// x
var pExp = Expression.Parameter(typeof(int), "x");
// minValue
var rightExp = Expression.Constant(minValue);
// x >= minValue
var 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)
{
// x
var 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)
{
// x
var 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)
{
// x
var 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;
}
}
還要更多
如果邏輯關係更複雜,有多層嵌套像樹形一樣,比較方法也很多花樣,甚至包含方法,怎麼辦?
可以參考以下示例:
如果你對此內容感興趣,還可以瀏覽我之前錄製的影片進行進一步了解:
-
戲精分享 C#表達式樹,第一季[2] -
戲精分享 C#表達式樹,第二季[3]
你也可以參閱之前一篇入門:
《只要十步,你就可以應用表達式樹來優化動態調用》[4]
或者看MSDN文檔,我覺得你也可以有所收穫:
這篇相關的程式碼,可以通過以下地址得到:
如果你覺得本文不錯,記得收藏、點贊、評論、轉發。告訴我還想知道點什麼喲。
參考資料
Newbe.ObjectVisitor: //github.com/newbe36524/Newbe.ObjectVisitor
[2]
戲精分享 C#表達式樹,第一季: //www.bilibili.com/video/BV15y4y1r7pK
[3]
戲精分享 C#表達式樹,第二季: //www.bilibili.com/video/BV1Mi4y1L7oR
[4]
只要十步,你就可以應用表達式樹來優化動態調用: //www.newbe.pro/Newbe.Claptrap/Using-Expression-Tree-To-Build-Delegate/index.html