简介
有些时候,我们需要动态构建一个比较复杂的查询条件,传入数据库中或者对集合进行查询。而条件本身可能来自前端请求,或者配置文件。那么使用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 来构造,而这比从程序集中反射出实例对象会更高效。