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},就是操作符、两个下划线、属性名。
<form asp-action="List" method="post" class="form-inline">
<div class="form-group">
<label class="col-md-4 col-xs-4 col-sm-4 control-label">日志类型:</label>
<div class="col-md-8 col-xs-8 col-sm-8">
<select id="Eq__LogOperType" name="Eq__LogOperType" class="form-control" asp-items="@operateTypes"></select>
</div>
</div>
<div class="form-group">
<label class="col-md-4 col-xs-4 col-sm-4 control-label">日期:</label>
<div class="col-md-8 col-xs-8 col-sm-8">
<input type="date" id="Gte__CreateDate" name="Gte__CreateDate" class="form-control" value="@queryCreateDateStart.ToDateString()" />
</div>
</div>
<div class="form-group">
<label class="col-md-4 col-xs-4 col-sm-4 control-label"> - </label>
<div class="col-md-8 col-xs-8 col-sm-8">
<input type="date" id="Lt__CreateDate" name="Lt__CreateDate" class="form-control" value="@queryCreateDateEnd.ToDateString()" />
</div>
</div>
<button class="btn btn-primary" type="submit">查询</button>
</form>
例如,日志类型查询条件要求日志类型等于所选择的类型。日志类的日志类型属性是LogOperType,等于的操作符是Eq,这样Id就是Eq__LogOperType。同样的操作日期在开始和结束日期范围内,开始和结束日期的Id分别为Gte__CreateDate和Lt__CreateDate。
2、编写ModelBinder,接收前端传来的参数,生成查询条件类。
这里,我们定义一个查询条件类,QueryConditionCollection,注释写的还是比较明确的:
/// <summary>
/// 操作条件集合
/// </summary>
public class QueryConditionCollection : KeyedCollection<string, QueryConditionItem>
{
/// <summary>
/// 初始化
/// </summary>
public QueryConditionCollection()
: base()
{
} /// <summary>
/// 从指定元素提取键
/// </summary>
/// <param name="item">从中提取键的元素</param>
/// <returns>指定元素的键</returns>
protected override string GetKeyForItem(QueryConditionItem item)
{
return item.Key;
}
} /// <summary>
/// 操作条件
/// </summary>
public class QueryConditionItem
{
/// <summary>
/// 主键
/// </summary>
public string Key { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } /// <summary>
/// 条件操作类型
/// </summary>
public QueryConditionType Op { get; set; } ///// <summary>
///// DataValue是否包含单引号,如'DataValue'
///// </summary>
//public bool IsIncludeQuot { get; set; } /// <summary>
/// 数据的值
/// </summary>
public object DataValue { get; set; }
}
按照我们的设计,上面日志查询例子应该产生一个QueryConditionCollection,包含三个QueryConditionItem,分别是日志类型、开始和结束日期条件项。可是,如何通过前端页面传来的请求数据生成QueryConditionCollection呢?这里就用到了ModelBinder。ModelBinder是MVC的数据绑定的核心,主要作用就是从当前请求提取相应的数据绑定到目标Action方法的参数上。
public class QueryConditionModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private const string SplitString = "__"; public QueryConditionModelBinder(IModelMetadataProvider metadataProvider)
{
_metadataProvider = metadataProvider;
} public async Task BindModelAsync(ModelBindingContext bindingContext)
{
QueryConditionCollection model = (QueryConditionCollection)(bindingContext.Model ?? new QueryConditionCollection()); IEnumerable<KeyValuePair<string, StringValues>> collection = GetRequestParameter(bindingContext); List<string> prefixList = Enum.GetNames(typeof(QueryConditionType)).Select(s => s + SplitString).ToList(); foreach (KeyValuePair<string, StringValues> kvp in collection)
{
string key = kvp.Key;
if (key != null && key.Contains(SplitString) && prefixList.Any(s => key.StartsWith(s, StringComparison.CurrentCultureIgnoreCase)))
{
string value = kvp.Value.ToString();
if (!string.IsNullOrWhiteSpace(value))
{
AddQueryItem(model, key, value);
}
}
} bindingContext.Result = ModelBindingResult.Success(model); //todo: 是否需要加上这一句?
await Task.FromResult();
} private void AddQueryItem(QueryConditionCollection model, string key, string value)
{
int pos = key.IndexOf(SplitString);
string opStr = key.Substring(, pos);
string dataField = key.Substring(pos + ); QueryConditionType operatorEnum = QueryConditionType.Eq;
if (Enum.TryParse<QueryConditionType>(opStr, true, out operatorEnum))
model.Add(new QueryConditionItem
{
Key = key,
Name = dataField,
Op = operatorEnum,
DataValue = value
});
}
}
主要流程是,从当前上下文中获取请求参数(Querystring、Form等),对于每个符合格式要求的请求参数生成QueryConditionItem并加入到QueryConditionCollection中。
为了将ModelBinder应用到系统中,我们还得增加相关的IModelBinderProvider。这个接口的主要作用是提供相应的ModelBinder对象。为了能够应用QueryConditionModelBinder,我们必须还要再写一个QueryConditionModelBinderProvider,继承IModelBinderProvider接口。
public class QueryConditionModelBinderPrivdier : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
} if (context.Metadata.ModelType != typeof(QueryConditionCollection))
{
return null;
} return new QueryConditionModelBinder(context.MetadataProvider);
}
}
下面就是是在Startup中注册ModelBinder。
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new QueryConditionModelBinderPrivdier());
});
3、将查询类转换为EF的查询Linq表达式。
我们的做法是在QueryConditionCollection类中编写方法GetExpression。这个只能贴代码了,里面有相关的注释,大家可以仔细分析下程序。
public Expression<Func<T, bool>> GetExpression<T>()
{
if (this.Count() == )
{
return c => true;
} //构建 c=>Body中的c
ParameterExpression param = Expression.Parameter(typeof(T), "c"); //获取最小的判断表达式
var list = Items.Select(item => GetExpression<T>(param, item));
//再以逻辑运算符相连
var body = list.Aggregate(Expression.AndAlso); //将二者拼为c=>Body
return Expression.Lambda<Func<T, bool>>(body, param);
} private Expression GetExpression<T>(ParameterExpression param, QueryConditionItem item)
{
//属性表达式
LambdaExpression exp = GetPropertyLambdaExpression<T>(item, param); //常量表达式
var constant = ChangeTypeToExpression(item, exp.Body.Type); //以判断符或方法连接
return ExpressionDict[item.Op](exp.Body, constant);
} private LambdaExpression GetPropertyLambdaExpression<T>(QueryConditionItem item, ParameterExpression param)
{
//获取每级属性如c.Users.Proiles.UserId
var props = item.Name.Split('.'); Expression propertyAccess = param; Type typeOfProp = typeof(T); int i = ;
do
{
PropertyInfo property = typeOfProp.GetProperty(props[i]);
if (property == null) return null;
typeOfProp = property.PropertyType;
propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
i++;
} while (i < props.Length); return Expression.Lambda(propertyAccess, param);
} #region ChangeType
/// <summary>
/// 转换SearchItem中的Value的类型,为表达式树
/// </summary>
/// <param name="item"></param>
/// <param name="conversionType">目标类型</param>
private Expression ChangeTypeToExpression(QueryConditionItem item, Type conversionType)
{
if (item.DataValue == null)
return Expression.Constant(item.DataValue, conversionType); #region 数组
if (item.Op == QueryConditionType.In)
{
var arr = (item.DataValue as Array);
var expList = new List<Expression>();
//确保可用
if (arr != null)
for (var i = ; i < arr.Length; i++)
{
//构造数组的单元Constant
var newValue = arr.GetValue(i);
expList.Add(Expression.Constant(newValue, conversionType));
} //构造inType类型的数组表达式树,并为数组赋初值
return Expression.NewArrayInit(conversionType, expList);
}
#endregion var value = conversionType.GetTypeInfo().IsEnum ? Enum.Parse(conversionType, (string)item.DataValue)
: Convert.ChangeType(item.DataValue, conversionType); return Expression.Convert(((Expression<Func<object>>)(() => value)).Body, conversionType);
}
#endregion #region SearchMethod 操作方法
private readonly Dictionary<QueryConditionType, Func<Expression, Expression, Expression>> ExpressionDict =
new Dictionary<QueryConditionType, Func<Expression, Expression, Expression>>
{
{
QueryConditionType.Eq,
(left, right) => { return Expression.Equal(left, right); }
},
{
QueryConditionType.Gt,
(left, right) => { return Expression.GreaterThan(left, right); }
},
{
QueryConditionType.Gte,
(left, right) => { return Expression.GreaterThanOrEqual(left, right); }
},
{
QueryConditionType.Lt,
(left, right) => { return Expression.LessThan(left, right); }
},
{
QueryConditionType.Lte,
(left, right) => { return Expression.LessThanOrEqual(left, right); }
},
{
QueryConditionType.Contains,
(left, right) =>
{
if (left.Type != typeof (string)) return null;
return Expression.Call(left, typeof (string).GetMethod("Contains"), right);
}
},
{
QueryConditionType.In,
(left, right) =>
{
if (!right.Type.IsArray) return null;
//调用Enumerable.Contains扩展方法
MethodCallExpression resultExp =
Expression.Call(
typeof (Enumerable),
"Contains",
new[] {left.Type},
right,
left); return resultExp;
}
},
{
QueryConditionType.Neq,
(left, right) => { return Expression.NotEqual(left, right); }
},
{
QueryConditionType.StartWith,
(left, right) =>
{
if (left.Type != typeof (string)) return null;
return Expression.Call(left, typeof (string).GetMethod("StartsWith", new[] {typeof (string)}), right); }
},
{
QueryConditionType.EndWith,
(left, right) =>
{
if (left.Type != typeof (string)) return null;
return Expression.Call(left, typeof (string).GetMethod("EndsWith", new[] {typeof (string)}), right);
}
}
};
#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中写一大堆的程序代码了,在条件多、甚至有可选条件时,优势更为明显。
9.1.2 asp.net core 自动生成组合查询的更多相关文章
- ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 使用 EF 框架查询数据 上一章节我们学习了如何设置 ...
- 第二十节:Asp.Net Core WebApi生成在线文档
一. 基本概念 1.背景 使用 Web API 时,了解其各种方法对开发人员来说可能是一项挑战. Swagger 也称为OpenAPI,解决了为 Web API 生成有用文档和帮助页的问题. 它具有诸 ...
- 几十行代码实现ASP.NET Core自动依赖注入
在开发.NET Core web服务的时候,我们习惯使用自带的依赖注入容器来进行注入. 于是就会经常进行一个很频繁的的重复动作:定义一个接口->写实现类->注入 有时候会忘了写Add这一步 ...
- asp.net webAPI 自动生成帮助文档并测试
之前在项目中有用到webapi对外提供接口,发现在项目中有根据webapi的方法和注释自动生成帮助文档,还可以测试webapi方法,功能很是强大,现拿出来与大家分享一下. 先看一下生成的webapi文 ...
- 自动生成 Lambda查询和排序,从些查询列表so easy
如下图查询页面,跟据不同条件动态生成lambda的Where条件和OrderBy,如果要增加或调整查询,只用改前台HTML即可,不用改后台代码 前台代码: <div style="pa ...
- 【MyBatis】MyBatis自动生成代码查询之爬坑记
前言 项目使用SSM框架搭建Web后台服务,前台后使用restful api,后台使用MyBatisGenerator自动生成代码,在前台使用关键字进行查询时,遇到了一些很宝贵的坑,现记录如下.为展示 ...
- asp.net core系列 32 EF查询数据 必备知识(1)
一.查询的工作原理 Entity Framework Core 使用语言集成查询 (LINQ) 来查询数据库中的数据. 通过 LINQ 可使用 C#(或你选择的其他 .NET 语言)基于派生上下文和实 ...
- ASP.NET Core Razor生成Html静态文件
一.前言 最近做项目的时候,使用Util进行开发,使用Razor写前端页面.初次使用感觉还是不大习惯,之前都是前后端分离的方式开发的,但是使用Util封装后的Angular后,感觉开发效率还是杠杠滴. ...
- 使用ef core自动生成mysql表和数据编码的问题
mysql默认的编码是不支持中文的,需要改成utf8编码格式. 而我使用的Pomelo.EntityFrameworkCore.MySql组件生成mysql库和表,他是使用默认编码的. 网上大多说修改 ...
随机推荐
- maven的pom.xml关系依赖书写顺序
今天遇到了一个情况,以前代码编译没有问题,升级了hbase客户端phoenix驱动,又调整了thrift的关系依赖的位置,放到了这个驱动后面. 如下: 导致了一个thrift接口类编译报错: 检查这个 ...
- MySQL加密
MySQL字段加密和解密 1.加密:aes_encrypt('admin','key') 解密:aes_decrypt(password,'key') 2.双向加密 通过密钥去加密,解密的时候的只有知 ...
- Linux基础介绍【第四篇】
Linux文件和目录的属性及权限 命令: [root@oldboy ~]# ls -lhi total 40K 24973 -rw-------. 1 root root 1.1K Dec 10 16 ...
- vim环境变量配置、背景色配置
我们使用vi或者vim的时候,如果想要显示行号,可能会这样做:切换到命令模式,然后输入set nu,再按回车键就显示了:还有就是咱们在编写程序的时候,有的时候会希望按下回车键后,光标不是每次都在行首, ...
- Linux实战教学笔记06:Linux系统基础优化
第六节 Linux系统基础优化 标签(空格分隔):Linux实战教学笔记-陈思齐 第1章 基础环境 第2章 使用网易163镜像做yum源 默认国外的yum源速度很慢,所以换成国内的. 第一步:先备份 ...
- Ubuntu 16.04 安装 arm-linux-gcc 嵌入式交叉编译环境 问题汇总
闲扯: 实习了将近半年一直在做硬件以及底层的驱动,最近要找工作了发现了对linux普遍要求很高,而且工作岗位也非常多,所以最近一些时间在时不时地接触linux. 正文:(我一时兴起开始写博客,准备不充 ...
- EChart系列:在echart3中使用百度地图扩展之后,如何获取到百度地图对象
最近做项目想要在百度地图上叠加显示echart的散点图,然后根据地图的缩放等级和区域范围要显示不同的散点图,这中间折腾了好久.功能要求包括: (1)底图使用百度地图: (2)可以在地图上叠加显示ech ...
- Quality 是什么?
Quality 是什么? 通常,我们谈及 Quality(质量)时,最常见的问题就是:Quality 是什么? 有很多业界先驱和研究人员已经回答了这个问题,我在这里并不会再给出一个新的答案.在学习总结 ...
- Linux Hadoop2.7.3 安装(单机模式) 一
Linux Hadoop2.7.3 安装(单机模式) 一 Linux Hadoop2.7.3 安装(单机模式) 二 java环境安装 http://www.cnblogs.com/zeze/p/590 ...
- TortoiseGit:记住用户名和密码
1.背景: 我们在使用 tortoisegit 工具时会无可避免的经常性 pull 和 push,这时通常要输入用户名和密码,由于麻烦,就有人提出了记住用户名和密码的需求... ... 2.设置: [ ...