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库和表,他是使用默认编码的. 网上大多说修改 ...
随机推荐
- MongoDB学习笔记六—查询下
查询内嵌文档 数据准备 > db.blog.find().pretty() { "_id" : ObjectId("585694e4c5b0525a48a441b5 ...
- Oozie分布式任务的工作流——Spark篇
Spark是现在应用最广泛的分布式计算框架,oozie支持在它的调度中执行spark.在我的日常工作中,一部分工作就是基于oozie维护好每天的spark离线任务,合理的设计工作流并分配适合的参数对于 ...
- Princeton Algorithms week3 Assignment
这周编程作业是实现检测点共线的算法.和排序算法有关系的地方在于,对斜率排序后可以很快的检测出来哪些点是共线的,另外这个算法的瓶颈也在于排序的性能. 一点收获: java传参数时传递的是值,这很多人都知 ...
- ASP.NET Core 性能对比评测(ASP.NET,Python,Java,NodeJS)
前言 性能是我们日常生活中经常接触到的一个词语,更好的性能意味着能给我们带来更好的用户体检.比如我们在购买手机.显卡.CPU等的时候,可能会更加的关注于这样指标,所以本篇就来做一个性能评测. 性能也一 ...
- C#高级知识点&(ABP框架理论学习高级篇)——白金版
前言摘要 很早以前就有要写ABP高级系列教程的计划了,但是迟迟到现在这个高级理论系列才和大家见面.其实这篇博客很早就着手写了,只是楼主一直写写停停.看看下图,就知道这篇博客的生产日期了,谁知它的出厂日 ...
- 【完全开源】知乎日报UWP版:增加Live磁贴、Badge、以及Toast通知
目录 说明 实现方法 APP生命期 后台任务 说明 之前网上有人建议增加磁贴(tile).徽章(badge)功能.利用周末的时间,将这两个功能添加上去了.如果将磁贴固定到开始屏幕,磁贴就会循环播放首页 ...
- 借助Nodejs探究WebSocket
文章导读: 一.概述-what's WebSocket? 二.运行在浏览器中的WebSocket客户端+使用ws模块搭建的简单服务器 三.Node中的WebSocket 四.socket.io 五.扩 ...
- 命令行查看Windows激活信息(win7、win8、win10...)
使用:Win+ R 组合键,打开运行命令框,复制命令,粘贴后回车. slmgr.vbs -xpr 查询Windows是否永久激活slmgr.vbs -dlv 查询到Windows的激活信息,包括:激活 ...
- PDO概念 分析 练习
PDO 翻译过来叫做数据访问抽象层 它是一个数据访问的层面,实际上是一个类,也就是说所有操作数据库的代码,都是通过这个层面完成的 该图好理解大概就是这样一种模式 现在考虑的是能不能使用同一个类,上层代 ...
- Hyper-V1:创建和管理虚拟机
Hyper-V是微软的管理虚拟机(Virtual Machine)的服务,在安装Hyper-V功能之后,系统自动安装可视化的虚拟机管理工具:Hyper-V Manager.在同一台物理机上,能够使用H ...