.Net C# Expression Tree

深入淺出 .NET表達樹機制 Expression Tree

莊智捷 Visy Chuang 2020/12/16 00:24:42
594

    在.NET的世界裡, 表達樹 (Expression Tree) 其實就是由許多表達式 (Lambda Expression) 節點所組成的樹狀結構,

因此在一探究竟何謂表達樹之前, 必須先理解什麼是"表達式"?

 

表達式 Lambda Expression

表達式通常有兩種寫法, 一種是陳述式 (statement lambda), 另一種是運算式 (expression lambda)

 

陳述式: 代表一段程式碼, 通常由大括弧包住

運算式: 在陳述式中"表達邏輯運算"的程式碼語句, 通常由常數, 變數, 函式呼叫等排列組合而成

 

運算式大都可以轉換成陳述式, 例如

(a, b) => a + b

上面運算式只要加入左右大刮弧及一個 return

(a, b) => { return a + b; }

就變成了一個陳述式

 

表達式範例:

3                   //常數表達式

a                  //變數表達式

a + 3           //二元運算子表達式

Math.Abs(a) //函式呼叫表達式

 

表達樹 Expression Tree

表達樹是一個樹狀結構的物件, 而樹狀結構中的每一個節點都代表一個獨立的表達式,

你可以將表達式進行編譯及執行, 且在表達樹中的建立節點(Expression Node)只能使用 Expression 類別

 

常見的Expression Node有:

ConstantExpression    //常數

ParameterExpression  //變數

MethodCallExpression //函式呼叫

MemberExpression     //成員

LambdaExpression     //Lambda表達式

BinaryExpression       //二元運算式

(完整的Expression 類別可以參考這裡)

 

那麼Lambda表達式純Expression在使用上有什麼差異呢?

讓我們用一個簡單的例子比較看看:

輸出"傳入參數 X 2 "

 

使用 Lambda Expression:

Expression <Func<int, int>> expr1 = n => n*2;
Func<int, int> fn = expr1.Compile();
Console.WriteLine(fn(10));

Line 1: 將 n => n*2 這個Lambda 運算式轉換成樹狀資料結構 expr1

Line 2: 將 expr1 這個樹狀結構逆向轉譯為程式碼, 並存入一個委派物件 fn

Line 3: 印出執行 fn(10) 的結果 20

 

使用 Expression:

ParameterExpression p = Expression.Parameter(typeof(int), "r");
ConstantExpression c = Expression.Constant(2);
BinaryExpression b = BinaryExpression.Multiply(c, p);
LambdaExpression l= Expression.Lambda(b, p);
Expression<Func<int, int>> e=(Expression<Func<int, int>>) l;
Console.WriteLine(e.Compile()(10));

Line 1: 建立一個變數節點 p, 並將變數命名為 "r"

Line 2: 建立一個常數節點 c, 並付值2

Line 3: 建立一個二元運算式節點 b, 並指定為乘法運算, 規則為 (常數節點c X 變數節點p )

Line 4: 將上述節點組合為一個Lambda 表達式 l (同時也為一個節點)

Line 5: 將 l 這個 Lambda 表達式 轉型為 Expression<TDelegate>物件 e

Line 6: 將 e 執行編譯同時帶入參數執行, 印出結果 20

 

比照兩種作法不難看出, 原來簡潔的Lambda語句, 也是拆分成各個節點組合而成的

 

實作範例

實作情境 (1):

有一位熟悉SQL命令的使用者, 習慣用 like 的方式進行搜尋( ex: 在input欄位輸入"ABC%"),

並希望能將這個功能增加到系統表單的搜尋篩選裡

分析:

身為.NET後端工程師的你, 一定不難想到可以利用字串的 Contains, StartsWith 和 EndsWith 進行處理,

但若處理的查詢條件比較複雜, 每次都需要在處理like的地方加以判斷,實在不是一個好做法,

這時後端經驗豐富的你突然想到, 有沒有機會把 Like 寫成擴充方法? 使用時只需要一行程式碼就能解決了?

實作範例:

這時Expression Tree就派上用場了! 不多贅述, 直接看程式碼

// 根據查詢內容是否有% 符號來決定Like的方式 , ex : 如果使用者輸入 gg% => DB的where條件要是[欄位] like N'gg%'        
public static IQueryable<T> Like<T>(this IQueryable<T> query, Expression<Func<T, string>> lambda, string param)
{
    // 解析Lambda的內容
    var body = lambda.Body as MemberExpression;
    if (body == null) { return query; }

    // 產生參數
    ParameterExpression paramSource = Expression.Parameter(query.ElementType, "m");
    Expression columnExp = Expression.Property(paramSource, body.Member.Name);

    if (string.IsNullOrEmpty(param)){ return query; }

    string queryString = param.Replace("%", "");            
    Expression paramQuery = Expression.Constant(queryString, typeof(string));
    MethodCallExpression method;

    if (param.StartsWith("%"))
    {
        //所產生的Lambda :  d => d.欄位.EndsWith(paramString)
        method = Expression.Call(columnExp, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), paramQuery);
    }            
    else if (param.EndsWith("%"))
    {
        //所產生的Lambda :  d => d.欄位.StartsWith(paramString)
        method = Expression.Call(columnExp, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), paramQuery);
    }
    else
    {
        //所產生的Lambda :  d => d.欄位.Contains(paramString)
        method = Expression.Call(columnExp, typeof(string).GetMethod("Contains", new[] { typeof(string) }), paramQuery);
    }

    return query.Where(Expression.Lambda<Func<T, bool>>(method, paramSource));
}

實作情境 (2):

承(1), 使用者又希望能將表單的資料利用欄位排序, 而且要可以升冪(Desc)或降冪排序(Asc), 最好還可以同時複數個欄位一起排序!

分析:

這時你團隊的前端工程師微微一笑, 想著有許多不差的現成套件可以利用, 便一口答應下需求,

但身為.NET後端工程師的你卻頗為苦惱, 多個欄位的排序可以使用 OrderBy 及 ThenBy 完成, 

但又要考慮升冪或降冪排序, 處理的判斷式好像複雜了許多, 有沒有辦法將排序整合成結構化的擴充方法?

實作範例:

我們可以利用Expression Tree將多個排序的命令產生並組合在一起, 程式碼如下

// 依據多欄位排序的集合呼叫對應的排序方法
public static IQueryable<T> OrderByMultiple<T>(this IQueryable<T> query, List<OrderByCol> cols)
{            
    foreach (var col in cols)
    {
        ParameterExpression paramSource = Expression.Parameter(query.ElementType, "m");
        Expression columnExp = Expression.Property(paramSource, col.colName);                

        // 決定要執行的排序Method
        string methodName = 
                    $@"{(cols.IndexOf(col) == 0 ? "Order" : "Then")}By{(col.sortType == OrderbyType.Desc ? "Descending" : "")}";                

        // 產生 Call Method 的節點
        var method = Expression.Call(
                    typeof(Queryable),
                    methodName,
                    new Type[] { query.ElementType, columnExp.Type },
                    query.Expression,
                    Expression.Quote(Expression.Lambda(columnExp, paramSource)));

        query = query.Provider.CreateQuery<T>(method);
    }

    return query;
}

public enum OrderbyType
{
    Asc,
    Desc
}

public class OrderByCol
{
    public OrderbyType sortType { get; set; }
    public string colName { get; set; }
}

接著讓我們來測試實作

測試資料:

 

首先測試多欄位排序(不含Like):

static void Main(string[] args)
{            
    using (var _db = new DemoEntities())
    {
        var members = _db.Member
            //.Like(x => x.Memo, "TEST%")
            .OrderByMultiple(new List<OrderByCol>() 
            {
                new OrderByCol(){ colName = nameof(Member.CRT_Date), sortType = OrderbyType.Desc },
                new OrderByCol(){ colName = nameof(Member.Id), sortType = OrderbyType.Asc }                        
            });

            ShowResult(members);
    }

    Console.ReadKey();
}

static void ShowResult<T>(IEnumerable<T> dataList)
{
    foreach (T data in dataList)
    {
        var showMsg = new List<string>();
        data.GetType().GetProperties().ToList().ForEach(p =>
        {
            showMsg.Add($@"{p.Name}: {p.GetValue(data, null)}");
        });
        Console.WriteLine(string.Join(", ", showMsg));
    }
}

輸出結果:

 

接著加入Like試試:

static void Main(string[] args)
{            
    using (var _db = new DemoEntities())
    {
        var members = _db.Member
            .Like(x => x.Memo, "TEST%")
            .OrderByMultiple(new List<OrderByCol>() 
            {
                new OrderByCol(){ colName = nameof(Member.CRT_Date), sortType = OrderbyType.Desc },
                new OrderByCol(){ colName = nameof(Member.Id), sortType = OrderbyType.Asc }                        
            });

            ShowResult(members);
    }

    Console.ReadKey();
}

輸出結果:

 

小結

以上兩個簡單的例子, 示範了如何利用Expression Tree簡化複雜的查詢處理, 希望能提供大家coding時更多不一樣的思路!

 
 
 
莊智捷 Visy Chuang