9.1.2 asp.net core 自动生成组合查询

在做系统的时候,经常遇到前台录入一大堆的查询条件,然后点击查询提交后台,在Controller里面生成对应的查询SQL或者表达式,数据库执行再将结果返回客户端。

例如如下页面,输入三个条件,日志类型、开始和结束日期,查询后台系统操作日志,并显示。

这种类似页面在系统中还是比较多的,通常情况下,我们会在cshtml中放上日志类型、开始、结束日期这三个控件,controller的action有对应的三个参数,然后在action、逻辑层或者仓储层实现将这三个参数转换为linq,例如转成c=>c.BeginDate>=beginDate && c.EndDate < endDate.AddDay(1) && c.OperType == operType。

这里有个小技巧,就是结束日期小于录入的结束日期+1天。一般大家页面中录入结束日期的时候都是只到日期,不带时分秒,例如结束日期为2016年1月31日,endDate 就是2016-01-31。其实这时候,大家的想法是到2016年1月31日23:59:59秒止。如果数据库中存储的是带时分秒的时间,例如2016-01-31 10:00:00.000,而程序中写的是c.EndDate < endDate的话,那么这个2016年1月31日零点之后的全不满足条件。所以,这里应该是小于录入的结束日期+1。

如果我们有更多的条件怎么办?如果有的条件允许为空怎么办?如果有几十个这样的页面的?难道要一个个的去写么?

基于以上的考虑,我们为了简化操作,编写了自动生成组合查询条件的通用框架。做法主要有如下几步:

  • 前端页面采用一定的格式设置html控件的Id和Name。
  • 编写ModelBinder,接收前台传来的参数,生成查询条件类
  • 将查询条件类转换为SQL语句(petapoco等框架)或者表达式(EF框架),我们.netcore下用的是ef,因此只说明表达式的做法。peta的我们在.net framework下也实现了,在此不做叙述。
  • 提交数据库执行

 

下面详细介绍下具体的过程。

1、前端页面采用一定的格式设置Html控件的Id和Name,这里我们约定的写法是{Op}__{PropertyName},就是操作符、两个下划线、属性名。

 1 <form asp-action="List" method="post" class="form-inline">
 2   <div class="form-group">
 3     <label class="col-md-4 col-xs-4 col-sm-4 control-label">日志类型:</label>
 4     <div class="col-md-8 col-xs-8 col-sm-8">
 5       <select id="Eq__LogOperType" name="Eq__LogOperType" class="form-control" asp-items="@operateTypes"></select>
 6     </div>
 7   </div>
 8   <div class="form-group">
 9     <label class="col-md-4 col-xs-4 col-sm-4 control-label">日期:</label>
10     <div class="col-md-8 col-xs-8 col-sm-8">
11       <input type="date" id="Gte__CreateDate" name="Gte__CreateDate" class="form-control" value="@queryCreateDateStart.ToDateString()" />
12     </div>
13   </div>
14   <div class="form-group">
15     <label class="col-md-4 col-xs-4 col-sm-4 control-label"> - </label>
16     <div class="col-md-8 col-xs-8 col-sm-8">
17       <input type="date" id="Lt__CreateDate" name="Lt__CreateDate" class="form-control" value="@queryCreateDateEnd.ToDateString()" />
18     </div>
19   </div>
20   <button class="btn btn-primary" type="submit">查询</button>
21 </form>

例如,日志类型查询条件要求日志类型等于所选择的类型。日志类的日志类型属性是LogOperType,等于的操作符是Eq,这样Id就是Eq__LogOperType。同样的操作日期在开始和结束日期范围内,开始和结束日期的Id分别为Gte__CreateDate和Lt__CreateDate。

2、编写ModelBinder,接收前端传来的参数,生成查询条件类。

这里,我们定义一个查询条件类,QueryConditionCollection,注释写的还是比较明确的:

 1     /// <summary>
 2     /// 操作条件集合
 3     /// </summary>
 4     public class QueryConditionCollection : KeyedCollection<string, QueryConditionItem>
 5     {
 6         /// <summary>
 7         /// 初始化
 8         /// </summary>
 9         public QueryConditionCollection()
10             : base()
11         {
12         }
13 
14         /// <summary>
15         /// 从指定元素提取键
16         /// </summary>
17         /// <param name="item">从中提取键的元素</param>
18         /// <returns>指定元素的键</returns>
19         protected override string GetKeyForItem(QueryConditionItem item)
20         {
21             return item.Key;
22         }
23     }
24 
25     /// <summary>
26     /// 操作条件
27     /// </summary>
28     public class QueryConditionItem
29     {
30         /// <summary>
31         /// 主键
32         /// </summary>
33         public string Key { get; set; }
34         /// <summary>
35         /// 名称
36         /// </summary>
37         public string Name { get; set; }
38 
39         /// <summary>
40         /// 条件操作类型
41         /// </summary>
42         public QueryConditionType Op { get; set; }
43 
44         ///// <summary>
45         ///// DataValue是否包含单引号,如'DataValue'
46         ///// </summary>
47         //public bool IsIncludeQuot { get; set; }
48 
49         /// <summary>
50         /// 数据的值
51         /// </summary>
52         public object DataValue { get; set; }
53     }

按照我们的设计,上面日志查询例子应该产生一个QueryConditionCollection,包含三个QueryConditionItem,分别是日志类型、开始和结束日期条件项。可是,如何通过前端页面传来的请求数据生成QueryConditionCollection呢?这里就用到了ModelBinder。ModelBinder是MVC的数据绑定的核心,主要作用就是从当前请求提取相应的数据绑定到目标Action方法的参数上。

 1     public class QueryConditionModelBinder : IModelBinder
 2     {
 3         private readonly IModelMetadataProvider _metadataProvider;
 4         private const string SplitString = "__";
 5 
 6         public QueryConditionModelBinder(IModelMetadataProvider metadataProvider)
 7         {
 8             _metadataProvider = metadataProvider;
 9         }
10 
11         public async Task BindModelAsync(ModelBindingContext bindingContext)
12         {
13             QueryConditionCollection model = (QueryConditionCollection)(bindingContext.Model ?? new QueryConditionCollection());
14 
15             IEnumerable<KeyValuePair<string, StringValues>> collection = GetRequestParameter(bindingContext);
16 
17             List<string> prefixList = Enum.GetNames(typeof(QueryConditionType)).Select(s => s + SplitString).ToList();
18 
19             foreach (KeyValuePair<string, StringValues> kvp in collection)
20             {
21                 string key = kvp.Key;
22                 if (key != null && key.Contains(SplitString) && prefixList.Any(s => key.StartsWith(s, StringComparison.CurrentCultureIgnoreCase)))
23                 {
24                     string value = kvp.Value.ToString();
25                     if (!string.IsNullOrWhiteSpace(value))
26                     {
27                         AddQueryItem(model, key, value);
28                     }
29                 }
30             }
31 
32             bindingContext.Result = ModelBindingResult.Success(model);
33 
34             //todo: 是否需要加上这一句?
35             await Task.FromResult(0);
36         }
37 
38         private void AddQueryItem(QueryConditionCollection model, string key, string value)
39         {
40             int pos = key.IndexOf(SplitString);
41             string opStr = key.Substring(0, pos);
42             string dataField = key.Substring(pos + 2);
43 
44             QueryConditionType operatorEnum = QueryConditionType.Eq;
45             if (Enum.TryParse<QueryConditionType>(opStr, true, out operatorEnum))
46                 model.Add(new QueryConditionItem
47                 {
48                     Key = key,
49                     Name = dataField,
50                     Op = operatorEnum,
51                     DataValue = value
52                 });
53         }
54   }

主要流程是,从当前上下文中获取请求参数(Querystring、Form等),对于每个符合格式要求的请求参数生成QueryConditionItem并加入到QueryConditionCollection中。

 

为了将ModelBinder应用到系统中,我们还得增加相关的IModelBinderProvider。这个接口的主要作用是提供相应的ModelBinder对象。为了能够应用QueryConditionModelBinder,我们必须还要再写一个QueryConditionModelBinderProvider,继承IModelBinderProvider接口。

 1     public class QueryConditionModelBinderPrivdier : IModelBinderProvider
 2     {
 3         public IModelBinder GetBinder(ModelBinderProviderContext context)
 4         {
 5             if (context == null)
 6             {
 7                 throw new ArgumentNullException(nameof(context));
 8             }
 9 
10             if (context.Metadata.ModelType != typeof(QueryConditionCollection))
11             {
12                 return null;
13             }
14 
15             return new QueryConditionModelBinder(context.MetadataProvider);
16         }
17     }

下面就是是在Startup中注册ModelBinder。

services.AddMvc(options =>

{

     options.ModelBinderProviders.Insert(0, new QueryConditionModelBinderPrivdier());

});

 

3、将查询类转换为EF的查询Linq表达式。

我们的做法是在QueryConditionCollection类中编写方法GetExpression。这个只能贴代码了,里面有相关的注释,大家可以仔细分析下程序。

 

  1         public Expression<Func<T, bool>> GetExpression<T>()
  2         {
  3             if (this.Count() == 0)
  4             {
  5                 return c => true;
  6             }
  7 
  8             //构建 c=>Body中的c
  9             ParameterExpression param = Expression.Parameter(typeof(T), "c");
 10 
 11             //获取最小的判断表达式
 12             var list = Items.Select(item => GetExpression<T>(param, item));
 13             //再以逻辑运算符相连
 14             var body = list.Aggregate(Expression.AndAlso);
 15 
 16             //将二者拼为c=>Body
 17             return Expression.Lambda<Func<T, bool>>(body, param);
 18         }
 19 
 20         private Expression GetExpression<T>(ParameterExpression param, QueryConditionItem item)
 21         {
 22             //属性表达式
 23             LambdaExpression exp = GetPropertyLambdaExpression<T>(item, param);
 24 
 25             //常量表达式
 26             var constant = ChangeTypeToExpression(item, exp.Body.Type);
 27 
 28             //以判断符或方法连接
 29             return ExpressionDict[item.Op](exp.Body, constant);
 30         }
 31 
 32         private LambdaExpression GetPropertyLambdaExpression<T>(QueryConditionItem item, ParameterExpression param)
 33         {
 34             //获取每级属性如c.Users.Proiles.UserId
 35             var props = item.Name.Split('.');
 36 
 37             Expression propertyAccess = param;
 38 
 39             Type typeOfProp = typeof(T);
 40 
 41             int i = 0;
 42             do
 43             {
 44                 PropertyInfo property = typeOfProp.GetProperty(props[i]);
 45                 if (property == null) return null;
 46                 typeOfProp = property.PropertyType;
 47                 propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
 48                 i++;
 49             } while (i < props.Length);
 50 
 51             return Expression.Lambda(propertyAccess, param);
 52         }
 53 
 54         #region ChangeType
 55         /// <summary>
 56         /// 转换SearchItem中的Value的类型,为表达式树
 57         /// </summary>
 58         /// <param name="item"></param>
 59         /// <param name="conversionType">目标类型</param>
 60         private Expression ChangeTypeToExpression(QueryConditionItem item, Type conversionType)
 61         {
 62             if (item.DataValue == null)
 63                 return Expression.Constant(item.DataValue, conversionType);
 64 
 65             #region 数组
 66             if (item.Op == QueryConditionType.In)
 67             {
 68                 var arr = (item.DataValue as Array);
 69                 var expList = new List<Expression>();
 70                 //确保可用
 71                 if (arr != null)
 72                     for (var i = 0; i < arr.Length; i++)
 73                     {
 74                         //构造数组的单元Constant
 75                         var newValue = arr.GetValue(i);
 76                         expList.Add(Expression.Constant(newValue, conversionType));
 77                     }
 78 
 79                 //构造inType类型的数组表达式树,并为数组赋初值
 80                 return Expression.NewArrayInit(conversionType, expList);
 81             }
 82             #endregion
 83 
 84             var value = conversionType.GetTypeInfo().IsEnum ? Enum.Parse(conversionType, (string)item.DataValue)
 85                 : Convert.ChangeType(item.DataValue, conversionType);
 86 
 87             return Expression.Convert(((Expression<Func<object>>)(() => value)).Body, conversionType);
 88         }
 89         #endregion
 90 
 91         #region SearchMethod 操作方法
 92         private readonly Dictionary<QueryConditionType, Func<Expression, Expression, Expression>> ExpressionDict =
 93             new Dictionary<QueryConditionType, Func<Expression, Expression, Expression>>
 94                 {
 95                     {
 96                         QueryConditionType.Eq,
 97                         (left, right) => { return Expression.Equal(left, right); }
 98                         },
 99                     {
100                         QueryConditionType.Gt,
101                         (left, right) => { return Expression.GreaterThan(left, right); }
102                         },
103                     {
104                         QueryConditionType.Gte,
105                         (left, right) => { return Expression.GreaterThanOrEqual(left, right); }
106                         },
107                     {
108                         QueryConditionType.Lt,
109                         (left, right) => { return Expression.LessThan(left, right); }
110                         },
111                     {
112                         QueryConditionType.Lte,
113                         (left, right) => { return Expression.LessThanOrEqual(left, right); }
114                         },
115                     {
116                         QueryConditionType.Contains,
117                         (left, right) =>
118                             {
119                                 if (left.Type != typeof (string)) return null;
120                                 return Expression.Call(left, typeof (string).GetMethod("Contains"), right);
121                             }
122                         },
123                     {
124                         QueryConditionType.In,
125                         (left, right) =>
126                             {
127                                 if (!right.Type.IsArray) return null;
128                                 //调用Enumerable.Contains扩展方法
129                                 MethodCallExpression resultExp =
130                                     Expression.Call(
131                                         typeof (Enumerable),
132                                         "Contains",
133                                         new[] {left.Type},
134                                         right,
135                                         left);
136 
137                                 return resultExp;
138                             }
139                         },
140                     {
141                         QueryConditionType.Neq,
142                         (left, right) => { return Expression.NotEqual(left, right); }
143                         },
144                     {
145                         QueryConditionType.StartWith,
146                         (left, right) =>
147                             {
148                                 if (left.Type != typeof (string)) return null;
149                                 return Expression.Call(left, typeof (string).GetMethod("StartsWith", new[] {typeof (string)}), right);
150 
151                             }
152                         },
153                     {
154                         QueryConditionType.EndWith,
155                         (left, right) =>
156                             {
157                                 if (left.Type != typeof (string)) return null;
158                                 return Expression.Call(left, typeof (string).GetMethod("EndsWith", new[] {typeof (string)}), right);
159                             }
160                      }
161                 };
162         #endregion

 

4、提交数据库执行并反馈结果

在生成了表达式后,剩下的就比较简单了。仓储层直接写如下的语句即可:

var query = this.dbContext.OperLogs.AsNoTracking().Where(predicate).OrderByDescending(o => o.CreateDate).ThenBy(o => o.OperLogId);

predicate就是从QueryConditionCollection.GetExpression方法中生成的,类似

Expression<Func<OperLogInfo, bool>> predicate = conditionCollection.GetExpression<OperLogInfo>();

QueryConditionCollection从哪里来呢?因为有了ModelBinder,Controller的Action上直接加上参数,类似

public async Task<IActionResult> List(QueryConditionCollection queryCondition) { ... }

 

至此,自动生成的组合查询就基本完成了。之后我们程序的写法,只需要在前端页面定义查询条件的控件,Controller的Action中加上QueryConditionCollection参数,然后调用数据库前将QueryConditionCollection转换为表达式就OK了。不再像以往一样在cshtml、Controller中写一大堆的程序代码了,在条件多、甚至有可选条件时,优势更为明显。

 

面向云的.net core开发框架

posted @ 2016-11-08 14:10  BenDan2002  阅读(4543)  评论(16编辑  收藏  举报