一般來說所有的系統都離不開查詢,通俗點說就是前後端互動。我們的系統無非都是通過實體的屬性作為條件進行查詢,那我們有什麼方法可以拼裝成類似sql中的where條件呢?在.Net的體系中,藉助Linq + Expression我們可以將查詢引數轉化為表示式進行查詢。
為簡單易懂,我這裡簡單建立一個產品類Product來說明:
public class Product { public int Id {get;set;} public string Name {get;set;} public decimal Price {get;set;} //庫存 public int Stock {get;set;} public Status Status {get;set;} //建立時間 public DateTime CreationTime {get;set;} } public enum Status { //在售 OnSale = 1, //下架 OffSale = 2 }
我們頁面需要通過商品名,庫存範圍,狀態和建立時間範圍來作為條件查詢指定的商品,這裡我們先定義我們的查詢類
public class ProductQuery { public string Name {get;set;}
//最小庫存 public int MinStock {get;set;} //最大庫存 public int MaxStock {get;set;} public Status Status {get;set;} //建立開始時間 public DateTime CreationStartTime {get;set;} //建立結束時間 public DateTime CreationEndTime {get;set;} }
有了查詢類,一般的思想是通過if ...else... 來拼裝條件進行查詢,試想一下,如果查詢條件很多的話,那我們豈不是要寫很長的程式碼?這種流水式的程式碼正是我們要避免的。如何抽象化實現我們需要的功能呢?抓住我們開頭說的重點,無非就是通過程式碼生成我們想要的表示式即可。如何生成,首先我們定義一個查詢介面和它的實現
/// <summary> /// 定義查詢引數 /// </summary> /// <typeparam name="TEntity">要查詢的實體型別</typeparam> public interface IQuery<TEntity> where TEntity : class { /// <summary> /// 獲取查詢條件 /// </summary> Expression<Func<TEntity, bool>> GenerateExpression(); } /// <summary> /// 定義查詢引數 /// </summary> /// <typeparam name="TEntity">要查詢的實體型別</typeparam> public class Query<TEntity> : IQuery<TEntity> where TEntity : class { /// <summary> /// 指定查詢條件 /// </summary> protected Expression<Func<TEntity, bool>> _expression; /// <summary> /// 建立一個新的 <see cref="Query{TEntity}"/> /// </summary> public Query() { } /// <summary> /// 建立一個指定查詢條件的<see cref="Query{TEntity}"/> /// </summary> /// <param name="expression">指定的查詢條件</param> public Query(Expression<Func<TEntity, bool>> expression) { _expression = expression; } /// <summary> /// 獲取查詢條件 /// </summary> public virtual Expression<Func<TEntity, bool>> GenerateExpression() { return _expression.And(this.GenerateQueryExpression());
}
}
我們這個介面主要作用是對TEntity的屬性生成想要的表示式,來看核心的GenerateQueryExpression方法實現
/// <summary> 生成查詢表示式 </summary>
/// <typeparam name="TEntity">要查詢的實體型別</typeparam> public static Expression<Func<TEntity, bool>> GenerateQueryExpression<TEntity>(this IQuery<TEntity> query) where TEntity : class { if (query == null) return null; var queryType = query.GetType(); var param = Expression.Parameter(typeof(TEntity), "m"); Expression body = null; foreach (PropertyInfo property in queryType.GetProperties()) { var value = property.GetValue(query); if (value is string) { var str = ((string)value).Trim(); value = string.IsNullOrEmpty(str) ? null : str; } Expression sub = null;
//針對QueryMode特性獲取我們指定要查詢的路徑 foreach (var attribute in property.GetAttributes<QueryModeAttribute>()) { var propertyPath = attribute.PropertyPath; if (propertyPath == null || propertyPath.Length == 0) propertyPath = new[] { property.Name }; var experssion = CreateQueryExpression(param, value, propertyPath, attribute.Compare); if (experssion != null) { sub = sub == null ? experssion : Expression.Or(sub, experssion); } } if (sub != null) { body = body == null ? sub : Expression.And(body, sub); } } if (body != null) return Expression.Lambda<Func<TEntity, bool>>(body, param); return null; } /// <summary> /// 生成對應的表示式 /// </summary> private static Expression CreateQueryExpression(Expression param, object value, string[] propertyPath, QueryCompare compare) { var member = CreatePropertyExpression(param, propertyPath); switch (compare) { case QueryCompare.Equal: return CreateEqualExpression(member, value); case QueryCompare.NotEqual: return CreateNotEqualExpression(member, value); case QueryCompare.Like: return CreateLikeExpression(member, value); case QueryCompare.NotLike: return CreateNotLikeExpression(member, value); case QueryCompare.StartWidth: return CreateStartsWithExpression(member, value); case QueryCompare.LessThan: return CreateLessThanExpression(member, value); case QueryCompare.LessThanOrEqual: return CreateLessThanOrEqualExpression(member, value); case QueryCompare.GreaterThan: return CreateGreaterThanExpression(member, value); case QueryCompare.GreaterThanOrEqual: return CreateGreaterThanOrEqualExpression(member, value); case QueryCompare.Between: return CreateBetweenExpression(member, value); case QueryCompare.GreaterEqualAndLess: return CreateGreaterEqualAndLessExpression(member, value); case QueryCompare.Include: return CreateIncludeExpression(member, value); case QueryCompare.NotInclude: return CreateNotIncludeExpression(member, value); case QueryCompare.IsNull: return CreateIsNullExpression(member, value); case QueryCompare.HasFlag: return CreateHasFlagExpression(member, value); default: return null; } } /// <summary> /// 生成MemberExpression /// </summary> private static MemberExpression CreatePropertyExpression(Expression param, string[] propertyPath) { var expression = propertyPath.Aggregate(param, Expression.Property) as MemberExpression; return expression; } /// <summary> /// 生成等於的表示式 /// </summary> private static Expression CreateEqualExpression(MemberExpression member, object value) { if (value == null) return null; var val = Expression.Constant(ChangeType(value, member.Type), member.Type); return Expression.Equal(member, val); } /// <summary> /// 生成Sql中的like(contain)表示式 /// </summary> private static Expression CreateLikeExpression(MemberExpression member, object value) { if (value == null) return null; if (member.Type != typeof(string)) throw new ArgumentOutOfRangeException(nameof(member), $"Member '{member}' can not use 'Like' compare"); var str = value.ToString(); var val = Expression.Constant(str); return Expression.Call(member, nameof(string.Contains), null, val); }
其他的表示式暫時忽略,相信以朋友們高超的智慧肯定不是什麼難事:)
從這兩個核心的方法中我們可以看出,主要是通過自定義的這個QueryModeAttribute來獲取需要比較的屬性和比較方法,看一下它的定義
/// <summary> /// 查詢欄位 /// </summary> [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class QueryModeAttribute : Attribute { /// <summary> /// 比較方式 /// </summary> public QueryCompare Compare { get; set; } /// <summary> /// 對應屬性路徑 /// </summary> public string[] PropertyPath { get; set; } /// <summary> /// 查詢欄位 /// </summary> public QueryModeAttribute(params string[] propertyPath) { PropertyPath = propertyPath; } /// <summary> /// 查詢欄位 /// </summary> public QueryModeAttribute(QueryCompare compare, params string[] propertyPath) { PropertyPath = propertyPath; Compare = compare; } } /// <summary> /// 查詢比較方式 /// </summary> public enum QueryCompare { /// <summary> /// 等於 /// </summary> [Display(Name = "等於")] Equal, /// <summary> /// 不等於 /// </summary> [Display(Name = "不等於")] NotEqual, /// <summary> /// 模糊匹配 /// </summary> [Display(Name = "模糊匹配")] Like, /// <summary> /// 不包含模糊匹配 /// </summary> [Display(Name = "不包含模糊匹配")] NotLike, /// <summary> /// 以...開頭 /// </summary> [Display(Name = "以...開頭")] StartWidth, /// <summary> /// 小於 /// </summary> [Display(Name = "小於")] LessThan, /// <summary> /// 小於等於 /// </summary> [Display(Name = "小於等於")] LessThanOrEqual, /// <summary> /// 大於 /// </summary> [Display(Name = "大於")] GreaterThan, /// <summary> /// 大於等於 /// </summary> [Display(Name = "大於等於")] GreaterThanOrEqual, /// <summary> /// 在...之間,屬性必須是一個集合(或逗號分隔的字串),取第一和最後一個值。 /// </summary> [Display(Name = "在...之間")] Between, /// <summary> /// 大於等於起始,小於結束,屬性必須是一個集合(或逗號分隔的字串),取第一和最後一個值。 /// </summary> [Display(Name = "大於等於起始,小於結束")] GreaterEqualAndLess, /// <summary> /// 包含,屬性必須是一個集合(或逗號分隔的字串) /// </summary> [Display(Name = "包含")] Include, /// <summary> /// 不包含,屬性必須是一個集合(或逗號分隔的字串) /// </summary> [Display(Name = "不包含")] NotInclude, /// <summary> /// 為空或不為空,可以為 bool型別,或可空型別。 /// </summary> [Display(Name = "為空或不為空")] IsNull, /// <summary> /// 是否包含指定列舉 /// </summary> [Display(Name = "是否包含指定列舉")] HasFlag, }
好的,那我們如何使用呢?很簡單,只需在我們的查詢類中繼承並且指定通過何種方式比較和比較的是哪個屬性即可
public class ProductQuery : Query<Product> { //指定查詢的屬性是Name,且條件是Like [QueryMode(QueryCompare.Like,nameof(Product.Name))] public string Name {get;set;} //最小庫存 //指定查詢的屬性是Stock,且條件是大於等與 [QueryMode(QueryCompare.GreaterThanOrEqual,nameof(Product.Stock))] public int MinStock {get;set;} //最大庫存 //指定查詢條件是Stock,且條件是小於等於 [QueryMode(QueryCompare.LessThanOrEqual,nameof(Product.Stock))] public int MaxStock {get;set;} //指定查詢條件是Status,且條件是等於 [QueryMode(QueryCompare.Equal,nameof(Product.Status))] public Status Status {get;set;} //建立開始時間 //指定查詢條件是CreationTime,且條件是大於等與 [QueryMode(QueryCompare.GreaterThanOrEqual,nameof(Product.CreationTime))] public DateTime CreationStartTime {get;set;} //建立結束時間 //指定查詢條件是CreationTime,且條件是小於等於 [QueryMode(QueryCompare.LessThanOrEqual,nameof(Product.CreationTime))] public DateTime CreationEndTime {get;set;} }
在使用Linq方法查詢時,比如呼叫基於IQueryable的Where方法時,我們可以封裝自己的Where方法
/// <summary> /// 查詢指定條件的資料 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="source"></param> /// <param name="query"></param> public static IQueryable<TEntity> Where<TEntity>(this IQueryable<TEntity> source, IQuery<TEntity> query) where TEntity : class {
//獲取表示式 var filter = query?.GenerateExpression(); if (filter != null) source = source.Where(filter); return source; }
這樣在我們的Controller裡面這樣寫
[HttpPost] public async Task<JsonResult> SearchProductList(ProductQuery query) { var data = await _productService.GetSpecifyProductListAsync(query); return Json(result); }
我們的service層這樣實現GetSpecifyProductListAsync
/// <summary> /// 獲取指定條件的商品 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="query"></param> /// <returns></returns> public Task<List<Product>> GetSpecifyProductListAsync<Product>(IQuery<Product> query = null) { return _productRepository.AsNoTracking().Where(query).ToListAsync(); }
這樣在前端傳過來的條件,都會自動通過我們核心的方法GenerateExpression生成的表示式作為條件進行查詢進而返回實體列表。當然,還可以有更高階的方法,比如返回的是分頁的資料,或者返回的是指定的型別(直接返回實體是不安全的),後續我們都會針對更高階的開發思想來講解到這些情況。
總結一下:
1. 建立我們的查詢實體(ProductQuery),指定我們的查詢屬性(Name, Status...)和查詢條件(QueryCompare)
2. 繼承我們的查詢實體Query,並且指定該次查詢是針對哪個資料實體(Query<Product>)
3. 封裝基於Linq的方法Where方法,這裡呼叫我們的核心方法GenerateExpression生成表示式
如果有更好的想法,歡迎探討。