本文唯一訪問地址:http://www.cnblogs.com/yubaolee/p/DynamicLinq.html
對於系統開發來說,按不同字段進行過濾查詢是一種常見的需求。在EF中通常的做法是:
/// <summary> /// 只是簡單舉例,只用了兩個過濾條件 /// </summary> IEnumerable<UserInfo> Search(string username = "", string usertype = "") { var query = _context.UserInfoes.AsQueryable(); if (!string.IsNullOrEmpty(username)) query = query.Where(u => u.UserName == username); if (!string.IsNullOrEmpty(usertype)) query = query.Where(u => u.UserType == usertype); return query.ToList(); }
這時如果我有一個新的需求,比如查詢用戶名中必須包含不定個數關鍵字的用戶。那我們可以用參數數組做類似下面的升級
private IEnumerable<UserInfo> Search(params string[] keys) { var query = _context.UserInfoes.AsQueryable(); foreach (var key in keys) { query = query.Where(u => u.UserName.Contains(key)); } return query.ToList(); }
上面的代碼都是能夠良好運行的,這時如果需求變成了:查詢用戶名中至少包含一個關鍵字的用戶,那我們該如何處理?很明顯要用到Or運算,但怎麼處理才是最合理的?普通的查詢已經不能很好的解決該問題。於是Joe Albahari大神在他的一篇博文中使用PredicateBuilder輕松地解決了該問題:
IQueryable<UserInfo> Search(params string[] keys) { var predicate = PredicateBuilder.False<UserInfo>(); foreach (string keyword in keys) { predicate = predicate.Or(p => p.UserName.Contains(keyword)); } return _context.UserInfoes.Where(predicate); }
至於PredicateBuilder的實現可以去他的博文中查看或者直接在nuget中查找添加LINQKit引用。PredicateBuilder很好的解決的動態生成Lambda問題,支持And/Or等主流運算。但它仍沒能解決一個問題:如果查詢條件中的屬性(即數據庫中的字段)也是不確定的,這樣該如何處理?
這時Scott大神站出來了。在他的博客Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library)中,他把EF整成了拼接SQL的方式來實現這個需求。如下:
private IQueryable<UserInfo> Search(string key, string value) { return _context.UserInfoes.Where("@0 ='@1'", key, value); }
這樣我們就不怕無盡變更的需求,想怎麼調用都可以:
var users = Search("UserNmae", "yubaolee"); //過濾用戶名 var users2 = Search("UserType", "administrator"); //過濾用戶類型
你也可以使用Key-Value之類的組合成更強大的查詢函數。然,世界上的事情總不是那麼美好的。你會在實踐中用著用著發現,丫竟然不支持 like,用著用著發現,丫竟然不支持guid等等。
唉!我去,我等向來拿來就用之流,竟然會碰到這種鳥事。還是自己動手吧!
好吧,下面才是博文主要內容,如果少年你沒看到下面,啧啧!著實有些可惜…
分析一下我們的需求:
根據上面的需求,我們可以借鑒一下PredicateBuilder的實現方式,用表達式樹來生成動態lambda,然後傳到ef的過濾條件中。如下:
public class Filter { public string Key { get; set; } //過濾的關鍵字 public string Value { get; set; } //過濾的值 public string Contract { get; set; }// 過濾的約束 比如:'<' '<=' '>' '>=' 'like'等 } public static class DynamicLinq { /// <summary> /// 創建lambda中的參數,即c=>c.xxx==xx 中的c /// </summary> public static ParameterExpression CreateLambdaParam<T>(string name) { return Expression.Parameter(typeof(T), name); } /// <summary> /// 創建linq表達示的body部分,即c=>c.xxx==xx 中的c.xxx==xx /// </summary> public static Expression GenerateBody<T>(this ParameterExpression param, Filter filterObj) { PropertyInfo property = typeof(T).GetProperty(filterObj.Key); //組裝左邊 Expression left = Expression.Property(param, property); //組裝右邊 Expression right = null; //todo: 下面根據需要,擴展自己的類型 if (property.PropertyType == typeof(int)) { right = Expression.Constant(int.Parse(filterObj.Value)); } else if (property.PropertyType == typeof(DateTime)) { right = Expression.Constant(DateTime.Parse(filterObj.Value)); } else if (property.PropertyType == typeof(string)) { right = Expression.Constant((filterObj.Value)); } else if (property.PropertyType == typeof(decimal)) { right = Expression.Constant(decimal.Parse(filterObj.Value)); } else if (property.PropertyType == typeof(Guid)) { right = Expression.Constant(Guid.Parse(filterObj.Value)); } else if (property.PropertyType == typeof(bool)) { right = Expression.Constant(filterObj.Value.Equals("1")); } else { throw new Exception("暫不能解析該Key的類型"); } //todo: 下面根據需要擴展自己的比較 Expression filter = Expression.Equal(left, right); switch (filterObj.Contract) { case "<=": filter = Expression.LessThanOrEqual(left, right); break; case "<": filter = Expression.LessThan(left, right); break; case ">": filter = Expression.GreaterThan(left, right); break; case ">=": filter = Expression.GreaterThanOrEqual(left, right); break; case "like": filter = Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) }), Expression.Constant(filterObj.Value)); break; } return filter; } /// <summary> /// 創建完整的lambda,即c=>c.xxx==xx /// </summary> public static LambdaExpression GenerateLambda(this ParameterExpression param, Expression body) { return Expression.Lambda(body, param); } /// <summary> /// 創建完整的lambda,為了兼容EF中的where語句 /// </summary> public static Expression<Func<T, bool>> GenerateTypeLambda<T>(this ParameterExpression param, Expression body) { return (Expression<Func<T, bool>>)(param.GenerateLambda(body)); } public static Expression AndAlso(this Expression expression, Expression expressionRight) { return Expression.AndAlso(expression, expressionRight); } public static Expression Or(this Expression expression, Expression expressionRight) { return Expression.Or(expression, expressionRight); } public static Expression And(this Expression expression, Expression expressionRight) { return Expression.And(expression, expressionRight); } }
來看看我們客戶端的調用:
//模擬過濾對象 var filters = new Filter[] { new Filter {Key = "UserName", Value = "yubaolee", Contract = "like"}, new Filter {Key = "UserType", Value = "administrator", Contract = "="} }; var param = DynamicLinq.CreateLambdaParam<UserInfo>("c"); Expression body = Expression.Constant(true); //初始默認一個true foreach (var filter in filters) { body = body.AndAlso(param.GenerateBody<UserInfo>(filter)); //這裡可以根據需要自由組合 } var lambda = param.GenerateTypeLambda<UserInfo>(body); //最終組成lambda var users = _context.UserInfoes.Where(lambda); //得到最終結果 Console.Read();
這時我們可以自由組合,但客戶端代碼量看起來好像不少。我們來優化封裝一下:
public static class DynamicExtention { public static IQueryable<T> Where<T>(this IQueryable<T> query, Filter[] filters) { var param = DynamicLinq.CreateLambdaParam<T>("c"); Expression body = Expression.Constant(true); //初始默認一個true foreach (var filter in filters) { body = body.AndAlso(param.GenerateBody<T>(filter)); //這裡可以根據需要自由組合 } var lambda = param.GenerateTypeLambda<T>(body); //最終組成lambda return query.Where(lambda); } }
最後看看我們客戶端的調用:
//模擬過濾對象 var filters = new Filter[] { new Filter {Key = "UserName", Value = "yubaolee", Contract = "like"}, new Filter {Key = "UserType", Value = "administrator", Contract = "="} }; var users = _context.UserInfoes.Where(filters); //得到最終結果 Console.Read();
代碼如此的干淨整潔。而且因為擴展的Where語句是基於泛型的,所以無論你的EF集合是哪種DbSet,都可以直接拿來使用。如果再把過濾類Filter功能深化,擴展成樹狀結構,那麼可以實現種組合查詢。哪怕是聯表查詢也不在話下。