使用 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 來構造,而這比從程序集中反射出實例對象會更高效。

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