簡介
有些時候,我們需要動態構建一個比較複雜的查詢條件,傳入數據庫中或者對集合進行查詢。而條件本身可能來自前端請求,或者配置文件。那麼使用C# 的表達式目錄樹動態構建Lambda 就可以派上用場。
一個案例
有這樣一個需求:
我們有這樣一個模型 User,有Id、Email、Name、Age、Sex 、Address等屬性,前端頁面需要對User 列表進行動態查詢。大概會構造出如下的json 作爲查詢:
// 對模型構造的查詢json
"dimensionsFilters": [
[
// 內層條件是 AND 關係
{
"fieldName": "Name", // 字段
"values": [ "Joe", "Jack" ], // value
"operator": "IN_LIST", // 操作
"type": "EXCLUDE" // 包含還是排除
},
{
"fieldName": "Age",
"operator": "GreaterThan",
"values":18,
"type": "INCLUDE"
}
], // 外層條件是 OR 的關係
[
{
"fieldName": "Address",
"operator": "CONTAINS",
"values":"XXX",
"type": "INCLUDE"
}
]
],
// 對模型數據範圍的過濾
"dateRange": {
"startDate": "2021-12-29",
"endDate": "2021-11-01"
},
從以上查詢片段可以分析出:
- 查詢條件可以動態拼接
- 需要支持的查詢操作有 集合包含、集合排除、大於、小於、大於等於、小於等於、等等
- 查詢條件可以動態調整AND 和OR 的關係
要實現以上需求,重點是解析表達式和動態拼接表達式,並且我們可以觀察出操作符都是比較簡單的對單個集合的操作。
實現方案
先來假設已經拼接好了filter , 那麼對User 的查詢可以用以下代碼表示:
var filter=xxx;
var result = items.Where(filter);
所以,重點是如何構造filter,我們可以觀察Linq 條件中的參數,Func<TSource, bool> predicate 這就是一個返回bool 的 Lambda表達式。
幸運的是,C# 爲我們提供了Expression 類來實現Lambda 表達式拼接。
對於兩個條件的And 拼接,無需特殊拼接,可以對結果集多次使用Where操作。
下面是兩個條件的Or拼接,使用Expression.Or 實現兩個表達式OR拼接:
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> exp1, Expression<Func<T, bool>> exp2)
{
var inokeExp = Expression.Invoke(exp2, exp1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.Or(exp1.Body, inokeExp), exp1.Parameters);
}
其他操作實際上是對Left 值 和Right 值的表達式構造,如:
- 相等操作:
// 該方法接收字段名,字段值,include表示是否包含該條件
public static Expression<Func<T, bool>> BuildEquals<T>(string fieldName, object constant, bool include)
{
// 聲明一個 T 類型的參數 m
var p1 = Expression.Parameter(typeof(T), "m");
// 得到 p1 的fieldName 屬性或者字段成員
var member = Expression.PropertyOrField(p1, fieldName);
// 得到一個表達式, 該表達式生成爲 constant.Equals(member) 的方法調用
var exp = Expression.Call(member, constant.GetType().GetMethod("Equals", new Type[] { constant.GetType() }),
new Expression[] { Expression.Constant(constant, constant.GetType()) });
if (include)
{
// 返回 Lambda 類型的表達式樹
return Expression.Lambda<Func<T, bool>>(exp, new ParameterExpression[] { p1 });
}
else
{
return Expression.Lambda<Func<T, bool>>(Expression.Not(exp), new ParameterExpression[] { p1 });
}
}
-
常量值的包含操作
調用string 的Contains方法:
public static Expression<Func<T, bool>> BuildContains<T>(string fieldName, string constant, bool include)
{
var p1 = Expression.Parameter(typeof(T), "m");
var member = Expression.PropertyOrField(p1, fieldName);
// 構成方法調用表達式 時 需要注意constant 類型需要有Contains 方法.
var exp = Expression.Call(member, typeof(string).GetMethod("Contains", new Type[] { typeof(string) }),
new Expression[] { Expression.Constant(constant, typeof(string)) });
if (include)
{
return Expression.Lambda<Func<T, bool>>(exp, new ParameterExpression[] { p1 });
}
else
{
return Expression.Lambda<Func<T, bool>>(Expression.Not(exp), new ParameterExpression[] { p1 });
}
}
-
集合的包含操作
使用list 的Contains 方法
public static Expression<Func<T, bool>> BuildInList<T, TItem>(string fieldName, List<TItem> list, bool include)
{
var p1 = Expression.Parameter(typeof(T), "m");
var member = Expression.PropertyOrField(p1, fieldName);
var conts = Expression.Constant(list, list.GetType());
var methodInfo = list.GetType().GetMethod("Contains");
// 構造list 調用 Contains 方法的表達式
var exp = Expression.Call(conts, methodInfo, member);
if (include)
{
return Expression.Lambda<Func<T, bool>>(exp, new ParameterExpression[] { p1 });
}
else
{
return Expression.Lambda<Func<T, bool>>(Expression.Not(exp), new ParameterExpression[] { p1 });
}
}
-
其他操作
System.Linq.Expressions 命名空間下,提供了一系列表達式類型,如:
BinaryExpression : 用於構造二元運算表達式MethodCallExpression:用於構造方法調用表達式
MemberExpression:生成成員
NewExpression:生成new 實例化方法
....
表達式樹與Lambda 表達式淺析
表達式(expression) 是由數字,運算符,括號、變量等組成,可以簡單看作能被求值的函數。
如:
x*y+(10-8)
x>y
p AND q
而 Lambda 表達式 可以看成返回爲bool 的委託,
如 :
var func = new Func<TestModel,bool>(item=>item.Id==1);
而使用Expression構造Lambda 表達式就是構造一棵表達式樹,然後編譯成一個委託方法。
如需要對“ item.Id ==1 ” 這個表達式構造表達式樹,步驟如下:
// 1. 得到一個類型參數
var p1 = Expression.Parameter(typeof(TestModel),"item");
// 2. 得到TestModel 類型的 的成員表達式(MemberExpression)
var member = Expression.PropertyOrField(p1, "Id");
// 3. 構造相等表達式 (BinaryExpression)
var constsExp = Expression.Constant(1);
var equalExp = Expression.Equal(member, consts);
// 4. 拼接Lambda 表達式
var lambdaExp = Expression.Lambda<Func<TestModel,bool>>(equalExp,new ParameterExpression[]{p1});
// 5.得到委託
var func = lambdaExp.Compile();
小結
-
使用Expression 表達式類,可以拼接出比較複雜的表達式,Linq 中的 Lambda 表達式只是它的一小部分能力。
-
很多使用反射的場景都可以使用 Expression.New 、Expression.Call 來構造,而這比從程序集中反射出實例對象會更高效。