使用 Expression (表达式树)动态构造Lambda 表达式

简介

有些时候,我们需要动态构建一个比较复杂的查询条件,传入数据库中或者对集合进行查询。而条件本身可能来自前端请求,或者配置文件。那么使用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"
  },
  
  

从以上查询片段可以分析出:

  1. 查询条件可以动态拼接
  2. 需要支持的查询操作有 集合包含、集合排除、大于、小于、大于等于、小于等于、等等
  3. 查询条件可以动态调整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();

小结

  1. 使用Expression 表达式类,可以拼接出比较复杂的表达式,Linq 中的 Lambda 表达式只是它的一小部分能力。

  2. 很多使用反射的场景都可以使用 Expression.New 、Expression.Call 来构造,而这比从程序集中反射出实例对象会更高效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章