本文唯一訪問地址: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功能深化,擴展成樹狀結構,那麼可以實現種組合查詢。哪怕是聯表查詢也不在話下。