一般来说所有的系统都离不开查询,系统的查询无非都是通过实体的属性作为条件进行查询,那我们有什么方法可以拼装成类似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生成表达式
如果有更好的想法,欢迎探讨。