C#動態構建表示式樹(三)——表示式的組合
C#動態構建表示式樹(三)——表示式的組合
前言
在篩選資料的過程中,可能會有這樣的情況:有一些查詢條件是公共的,但是根據具體的傳入引數可能需要再額外增加一個條件。對於這種問題一般有兩種方法:
a. 在 Where 後再組合一個 Where,如:
List<SOME_CLASS> dataList = dataList.Where(FILTER_1).Where(FILTER_2).ToList();
b. 將型別相同兩個表示式組合起來(就是本文的主題了)
由於專案中既有框架的封裝,查詢時只能傳入 Expression<Func<T, bool>> 型別的值,因此只能採用 b 方法。
最終
先給出研究後最後得出的方法,再分享自己踩坑的過程(僅以 And 操作為例,其它的大同小異)。
1. 程式碼
public static Expression<Func<T, bool>> CombineLambda<T>(List<Expression<Func<T, bool>>> lambdas, List<Type> types) { if(typeof(T) != types[0]) { throw new Exception("型別列表的第一個值必須為泛型 T 的型別"); } List<ParameterExpression> parameterExpressions = new List<ParameterExpression>(); // 從 'A' 開始,為每個型別指定一個形參 for (int i = 0; i < types.Count; i++) { parameterExpressions.Add(Expression.Parameter(types[i], ((char)(65 + i)).ToString())); } CombineExpressionVisitor visitor = new CombineExpressionVisitor(parameterExpressions); // 新建一個原始條件 a => true,並逐個組合陣列中的條件 Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>( Expression.Constant(true), parameterExpressions[0]); for (int i = 0; i < lambdas.Count; i++) { Expression tmp = visitor.Visit(result.Body); Expression tmp1 = visitor.Visit(lambdas[i].Body); result = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(tmp, tmp1), parameterExpressions[0]); } return result; /* ** 上面的 type[0](parameterExpressions[0]) 可能比較費解。由於我們的情況是巢狀的對 ** 象,因此需要傳入 “外層物件型別的 Paramter” 和 “內層物件型別的 Parameter”, ** parameterExpressions[0] 表示的是 外層物件型別的 Parameter,由它開始組成** 整個的篩選 */ } private class CombineExpressionVisitor : ExpressionVisitor { private List<ParameterExpression> peList; public CombineExpressionVisitor(List<ParameterExpression> list) { peList = list; } // 覆寫 VisitParameter 方法,返回我們指定的統一 Parameter protected override Expression VisitParameter(ParameterExpression p) { return peList.Find(a => a.Type == p.Type); } public override Expression Visit(Expression node) { return base.Visit(node); } }
Lambda表示式由引數(Parameter)和內容(Body)組成。主要的思路是 將給出的所有 Lambda 表示式的 引數(Parameter) 都改為 同一個引數(Parameter),再把內容(Body)組合起來 。
2. 驗證
假設我們有這樣的資料:
[ { "Name": "安柏", "Age": 25, "Weapons": [ { "Name": "普通弓", "GetTime": "2021-05-03T12:00:00+08:00", "Description": "入門級武器,從無到有" } ] }, { "Name": "行秋", "Age": 18, "Weapons": [ { "Name": "單手劍", "GetTime": "2021-05-22T11:00:00+08:00", "Description": "剛剛夠用的武器" } ] }, { "Name": "可莉", "Age": 8, "Weapons": [ { "Name": "嘟嘟可故事集", "GetTime": "2021-06-20T15:35:00+08:00", "Description": "特別合適的武器" }, { "Name": "簡單的書", "GetTime": "2021-05-03T16:47:00+08:00", "Description": "贈送的武器" } ] } ]
(原來你也玩原船,,,哦不是,原神啊)。主要有 “角色”、“年齡”(我瞎編的)、“武器”(半真半編的)這幾個欄位,其中 “武器”是單獨一個類,定義如下:
/// <summary>
/// 原神角色
/// </summary>
public class YuanshenRole
{
/// <summary>
/// 名字
/// </summary>
public string Name { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; set; }
/// <summary>
/// 所擁有的武器
/// </summary>
public List<Weapon> Weapons { get; set; }
}
/// <summary>
/// 武器
/// </summary>
public class Weapon
{
/// <summary>
/// 名稱
/// </summary>
public string Name { get; set; }
/// <summary>
/// 獲得的時間
/// </summary>
public DateTime GetTime { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
}
我們要查詢 Age 為 8, Name 為“可莉”,Weapons中存在 “嘟嘟可故事集” 的角色,分成兩個表示式寫,並組合起來:
// 三個單獨的條件
Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
Expression<Func<YuanshenRole, bool>> filter1 = x => x.Name == 可莉";
Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
// 組合起來
var finalFilter = CombineLambda(
new List<Expression<Func<YuanshenRole, bool>>> { filter, filter1, filter2 },
new List<Type> { typeof(YuanshenRole), typeof(Weapon) }
);
CombineLambda 方法需要兩個引數,一個是 表示式的集合,另一個是 表示式中 Parameter 型別的集合。
踩坑過程
1. 只拼接Lambda表示式的Body,不改變其引數
說起拼接,我的第一想法是這樣的:
// 錯誤的寫法
Expression<Func<YuanshenRole, bool>> finalFitler = Expression.Lambda<Func<YuanshenRole, bool>>(
Expression.AndAlso(filter.Body, filter1.Body), // 即使取了表示式的 Body,意義也不對
Expression.Parameter(typeof(YuanshenRole), "x")
);
報錯為:
從作用域“”引用了“UsageDemo.Program+YuanshenRole”型別的變數“x”,但該變數未定義”
在查詢資料的時候看到了一個比喻:每個 Lambda 表示式就像是一個獨輪車,把它們拼起來就相當於把兩個獨輪車拼成一個自行車。而上述的過程,我們看似把兩個輪子拆下來,安裝在了同一個車架子上,卻沒有改造兩個輪子讓其適應新的車架子(即,改變 “原有表示式 Body” 中引用 “原來Parameter” 的部分)
自然而然地,我們需要進行一番深入的改造。搜尋網上的資料後發現,可以通過繼承 ExpressionVisitor 類並覆寫其中的部分方法,如下:
class MyExpressionVisitor : ExpressionVisitor
{
public ParameterExpression _Parameter0 { get; set; }
public MyExpressionVisitor(ParameterExpression Parameter0)
{
_Parameter0 = Parameter0;
}
protected override Expression VisitParameter(ParameterExpression p)
{
return _Parameter0;
}
public override Expression Visit(Expression node)
{
return base.Visit(node);
}
}
VisitParameter 方法在 Visit 時預設返回原 Parameter,通過覆寫它可以達到改造 Body 中引用 “原來Parameter” 的效果。
ExpressionVisitor 中的 Visit 方法感覺與常規方法思路完全不同,先挖個坑,以後再研究
順理成章地,踩了第二個坑QAQ
2. 只傳入了 1 個 Parameter,而實際需要 2 個
根據踩坑1,寫法應該為:
Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
ParameterExpression pe = Expression.Parameter(typeof(YuanshenRole), "x");
var visitor1 = new MyExpressionVisitor(pe);
Expression bodyone1 = visitor1.Visit(filter.Body); // 此處需要呼叫我們自定類的 Visit 方法改造原 Parameter
Expression bodytwo1 = visitor1.Visit(filter2.Body);
Expression<Func<YuanshenRole, bool>> finalFilter =
ExpressionLambda<Func<YuanshenRole, bool>>(Expression.AndAlso(bodyone1,bodytwo1), pe);
dataList = dataList.AsQueryable().Where(finalFilter).ToList();
結果報錯為:
沒有為型別“UsageDemo.Program+YuanshenRole”定義屬性“System.String Name””
發生甚麼事了,我們的 filter 也沒有獲取 YuanshenRole 的 Name 屬性啊。此處我確實愣了一下,然後發現其實 查詢的是 Weapon 型別的 Name,而傳入的是 YuanshenRole 型別的引數。
因此在覆寫的 Visit 方法中不能只返回一個型別,而要根據實際情況返回。稍加封裝就有了文字最開始的程式碼。
private class CombineExpressionVisitor : ExpressionVisitor
{
......
public CombineExpressionVisitor(List<ParameterExpression> list)
{
peList = list;
}
......
這也是為什麼 Visit 類的建構函式要傳入 List<ParameterExpression> 的原因。
後記
最一開始我覺得合併兩個表示式應該是個很簡單的操作,可能一個方法就搞定了,沒想到他不講武德,讓我搞了這麼長時間。我大 E 了啊,沒有閃。希望這些程式碼以後耗子喂汁,不要再搞這樣的聰明,小聰明啊,謝謝朋友們!
參考
LINQ系列(7)——表示式樹之EXPRESSIONVISITOR
合併兩個 Lambda 表示式(此文中還介紹了通過 Invoke 方法來達到上述目的,但不適用於 IQueryable 型別的操作)