• 使用 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 来构造,而这比从程序集中反射出实例对象会更高效。

  • 相关阅读:
    大数据数仓数据分层名次介绍
    php 解压zip 格式的文件 何苦
    php 命令行执行 何苦
    VSCode使用笔记 何苦
    K8S Cilium网络插件——安装(v1.11.x版本)
    产品
    如何做好餐饮
    写作
    历史
    本能
  • 原文地址:https://www.cnblogs.com/aimigi/p/16035107.html
Copyright © 2020-2023  润新知