深入淺出 .NET表達樹機制 Expression Tree
在.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時更多不一樣的思路!