在做系统的时候,经常遇到前台录入一大堆的查询条件,然后点击查询提交后台,在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中写一大堆的程序代码了,在条件多、甚至有可选条件时,优势更为明显。

面向云的.net core开发框架

9.1.2 asp.net core 自动生成组合查询的更多相关文章

  1. ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 使用 EF 框架查询数据 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 使用 EF 框架查询数据 上一章节我们学习了如何设置 ...

  2. 第二十节:Asp.Net Core WebApi生成在线文档

    一. 基本概念 1.背景 使用 Web API 时,了解其各种方法对开发人员来说可能是一项挑战. Swagger 也称为OpenAPI,解决了为 Web API 生成有用文档和帮助页的问题. 它具有诸 ...

  3. asp.net webAPI 自动生成帮助文档并测试

    之前在项目中有用到webapi对外提供接口,发现在项目中有根据webapi的方法和注释自动生成帮助文档,还可以测试webapi方法,功能很是强大,现拿出来与大家分享一下. 先看一下生成的webapi文 ...

  4. 自动生成 Lambda查询和排序,从些查询列表so easy

    如下图查询页面,跟据不同条件动态生成lambda的Where条件和OrderBy,如果要增加或调整查询,只用改前台HTML即可,不用改后台代码 前台代码: <div style="pa ...

  5. 【MyBatis】MyBatis自动生成代码查询之爬坑记

    前言 项目使用SSM框架搭建Web后台服务,前台后使用restful api,后台使用MyBatisGenerator自动生成代码,在前台使用关键字进行查询时,遇到了一些很宝贵的坑,现记录如下.为展示 ...

  6. asp.net core系列 32 EF查询数据 必备知识(1)

    一.查询的工作原理 Entity Framework Core 使用语言集成查询 (LINQ) 来查询数据库中的数据. 通过 LINQ 可使用 C#(或你选择的其他 .NET 语言)基于派生上下文和实 ...

  7. ASP.NET Core Razor生成Html静态文件

    一.前言 最近做项目的时候,使用Util进行开发,使用Razor写前端页面.初次使用感觉还是不大习惯,之前都是前后端分离的方式开发的,但是使用Util封装后的Angular后,感觉开发效率还是杠杠滴. ...

  8. 使用ef core自动生成mysql表和数据编码的问题

    mysql默认的编码是不支持中文的,需要改成utf8编码格式. 而我使用的Pomelo.EntityFrameworkCore.MySql组件生成mysql库和表,他是使用默认编码的. 网上大多说修改 ...

  9. asp.net core webapi 生成导出excel

    /// <summary> /// 下载订单 /// </summary> /// <param name="model"></param ...

随机推荐

  1. js实现动态操作table

     本章案例为通过js,动态操作table,实现在单页面进行增删改查的操作. 简要案例如下: <%@ page language="java" contentType=&quo ...

  2. apache 的工作模式

    总结:访问量大的时候使用 worker模式:  每个进程,启动多个线程来处理请求,每个线程处理一次请求,对内存要求比较高. prefoek模式 : 每个子进程只有一个线程,一次请求一个进程. 什么是a ...

  3. NYOJ:题目524 A-B Problem

    题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=860 My思路: 先用两个字符串储存这两个实数,然后再用另外两个字符串储存去掉符号和前后多 ...

  4. Linux unzip解压文件到某个目录下面

    1,例如我想解压Metinfo5.2.zip  到某一个文件夹下,执行下面的命令就可以了 sudo unzip  MetInfo5.2.zip  -d  metinfo-bak

  5. 文本编辑器 CKEditor 用法

    最新文本编辑器,FCK升级版:CKEditor.NET CKEditor.NET.dll 版本:3.6.4.0 官方网址:http://ckeditor.com/   效果图:     配置web.c ...

  6. 【C#版本详情回顾】C#2.0主要功能列表

    泛型 优点:类型安全/重用代码/提升性能 应用:泛型接口.泛型类.泛型类型参数.泛型方法.泛型事件和泛型委托 命名空间:System.Collections.Generic 特性:泛型约束,defau ...

  7. iOS 防止UIButton重复点击

    使用UIButton的enabled或userInteractionEnabled 使用UIButton的enabled属性, 在点击后, 禁止UIButton的交互, 直到完成指定任务之后再将其en ...

  8. 理解Array.prototype.slice.call(arguments)

    在很多时候经常看到Array.prototype.slice.call()方法,比如Array.prototype.slice.call(arguments),下面讲一下其原理: 1.基本讲解 1.在 ...

  9. JavaScript基本操作之——九个 Console 命令

    一.显示信息的命令 console.log('hello'); console.info('信息'); console.error('错误'); console.warn('警告'); 二.占位符 c ...

  10. English trip EM2-LP-3A Gifts Teacher:Patrick

    课上内容(Lesson) 词汇(Key Word ) Identify   vt. 确定:识别:使参与:把…看成一样 objects  n. 物体(object的复数):目标  # UFO   =   ...