ASP.NET MVC 引入了 ModelBinder 技术,让我们可以在 Action 中以强类型参数的形式接收 Request 中的数据,极大的方便了我们的编程,提高了生产力。在查询 Action 中,我们可以将 Expression Trees 用作参数,通过自定义的 ModelBinder 动态自动构建查询表达式树,进一步发挥 MVC 的威力,简化编码工作。

先给出本文中使用的 Model:

1
2
3
4
5
6
7
8
public class Employee {
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Sex { get; set; }
public DateTime? Birthday { get; set; }
public string Remark { get; set; }
}

MVC 查询和存在的不足

下面是一个查询 Employee 的 Action,在 MVC 项目中经常可以见到:

1
2
3
4
5
6
7
8
9
10
11
12
public ActionResult Index(string firstName, string lastName, DateTime? birthday, bool? sex) {
var employees = repository.Query();
if (firstName.IsNotNullAndEmpty())
employees = employees.Where(e => e.FirstName.Contains(firstName));
if (firstName.IsNotNullAndEmpty())
employees = employees.Where(e => e.LastName.Contains(lastName));
if (birthday.HasValue)
employees = employees.Where(e => e.Birthday.Value.Date == birthday.Value.Date);
if (sex.HasValue)
employees = employees.Where(e => e.Sex == sex);
return View(employees);
}

得益于 MVC 的绑定技术,我们可以简单通过 Action 的参数来获取请求的值,很少再使用 Request["XXXX"] 的方式。

仔细观察,会发现上面这个 Action 中充斥着大量 if 判断,以致代码行数比较多,不是特别清晰。可以借助本人《c# 扩展方法奇思妙用基础篇 六:WhereIf 扩展》一文中的扩展方法予以简化:

1
2
3
4
5
6
7
8
public ActionResult Index2(string firstName, string lastName, DateTime? birthday, bool? sex) {
var employees = repository.Query()
.WhereIf(e => e.FirstName.Contains(firstName), firstName.IsNotNullAndEmpty())
.WhereIf(e => e.LastName.Contains(lastName), lastName.IsNotNullAndEmpty())
.WhereIf(e => e.Birthday.Value.Date == birthday.Value.Date, birthday.HasValue)
.WhereIf(e => e.Sex == sex, sex.HasValue);
return View("Index", employees);
}

代码相清晰了许多,我之前的几个 MVC 项目中也是这样处理的。

但时间一长,我逐步也发现了这种方式一些不足之处:

  1. 首先,网站中有很多类似的查询,如Customer、Order、Product 等等。而且大致也有点规律:字符串的一般模糊查询,时间日期类的一般按日期查询(忽略时间),其它类型则相等查询。不同 Model 查询的 Action 编码总有八、九分相似,但又不是简单的重复,却又难以重构
  2. 需求变动,如增加一个查询条件,修改 View 是必须的,但也要修改 Action,增加一个参数,还要加一行 Where 或 WhereIf。简单变动却多处修改,烦人啊,而且这种需求变动又是比较频繁的,尤其是在项目初期。若能只修改 View 而不修改 Action 就爽了。

思考后,我决定使用 Expression Trees 作为查询 Action的参数来弥补这些不足。

使用 Expression<Func<T, bool>> 作为 Action 的参数

试看如下代码:

1
2
3
4
public ActionResult Index3(Expression<Func<Employee, bool>> predicate) {
var employees = repository.Query().Where(predicate);
return View("Index", employees);
}

我将 Expression Trees 作为 Action 的唯一的参数(暂不考虑分页、排序等),将所有的查询条件都统一汇集至  predicate 参数。

所有的查询(不管是 Employee 还是 Customer)都使用如上代码。其它实体查询只需修改参数的类型,如 Customer 查询改为 Expression<Func<Customer, bool>> 。

细心品味下,相信你能理解这种做法的精妙之处!

如上修改代码后,直接运行会报错,因为 MVC 中默认的数据绑定器 DefaultModelBinder 不能正确绑定 Expression<Func<T, bool>> 类型的参数。

我们要新创一个新的 ModelBinder。

创建 QueryConditionExpressionModelBinder

我们需要一个新的 ModelBinder 来为 Expression<Func<T, bool>> 类型的参数赋值,且命名为 QueryConditionExpressionModelBinder。

QueryConditionExpressionModelBinder 要根据上下文来自动生成查询的 Expression Trees。主要关注的上下文有两点:首先是当前 Model 的类型,即 typeof(T);其次是 Request 提供的值,可通过 ValueProvider 获取。

下面给出一个粗略实现,仅用来说明这个思路是可行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class QueryConditionExpressionModelBinder : IModelBinder {
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var modelType = GetModelTypeFromExpressionType(bindingContext.ModelType);
if (modelType == null) return null; var body = default(Expression);
var parameter = Expression.Parameter(modelType, modelType.Name); foreach (var property in modelType.GetProperties()){
var queryValue = GetValueAndHandleModelState(property, bindingContext.ValueProvider, controllerContext.Controller);
if (queryValue == null) continue; Expression proeprtyCondition = null;
if (property.PropertyType == typeof (string)){
if (!string.IsNullOrEmpty(queryValue as string)){
proeprtyCondition = parameter
.Property(property.Name)
.Call("Contains", Expression.Constant(queryValue));
}
}
else if (property.PropertyType == typeof (DateTime?)){
proeprtyCondition = parameter
.Property(property.Name)
.Property("Value")
.Property("Date")
.Equal(Expression.Constant(queryValue));
}
else{
proeprtyCondition = parameter
.Property(property.Name)
.Equal(Expression.Constant(queryValue));
}
if (proeprtyCondition != null)
body = body != null ? body.AndAlso(proeprtyCondition) : proeprtyCondition;
}
if (body == null) body = Expression.Constant(true);
return body.ToLambda(parameter);
}
/// <summary>
/// 获取 Expression<Func<TXXX, bool>> 中 TXXX 的类型
/// </summary>
private Type GetModelTypeFromExpressionType(Type lambdaExpressionType) { if (lambdaExpressionType.GetGenericTypeDefinition() != typeof (Expression<>)) return null; var funcType = lambdaExpressionType.GetGenericArguments()[0];
if (funcType.GetGenericTypeDefinition() != typeof (Func<,>)) return null; var funcTypeArgs = funcType.GetGenericArguments();
if (funcTypeArgs[1] != typeof (bool)) return null;
return funcTypeArgs[0];
}
/// <summary>
/// 获取属性的查询值并处理 Controller.ModelState
/// </summary>
private object GetValueAndHandleModelState(PropertyInfo property, IValueProvider valueProvider, ControllerBase controller) {
var result = valueProvider.GetValue(property.Name);
if (result == null) return null; var modelState = new ModelState {Value = result};
controller.ViewData.ModelState.Add(property.Name, modelState); object value = null;
try{
value = result.ConvertTo(property.PropertyType);
}
catch (Exception ex){
modelState.Errors.Add(ex);
}
return value;
}
}

了解这段代码,需要 MVC 和 Expression Trees 的一些知识。这段代码还用到了 Expression 扩展方法,参见:《c# 扩展方法奇思妙用基础篇九:Expression 扩展》。

如果不想在 Global.asax 文件中设置 Expression<Func<T, bool>> 的 ModelBinder, 可以借助用下面这个 Attribute 类:

1
2
3
4
5
public class QueryConditionBinderAttribute : CustomModelBinderAttribute {
public override IModelBinder GetBinder() {
return new QueryConditionExpressionModelBinder();
}
}

Index3 简单修改如下:

1
public ActionResult Index3([QueryConditionBinder]Expression<Func<Employee, bool>> predicate) { //... }

下面是一个调试截图,绑定正常。

再次说明:本部分代码仅用来说明思路可行,用了大量的硬编码。

我也正在准备编写一个更加灵活 QueryConditionExpressionModelBinder,来应对复杂的查询(如时间范围、值大于、小于等、以及限制对某些属性的查询),目前也有了一个大体的思路,初步完成后在之后的博文中和大家分享下。如果你有好的思路,不妨写在回复中。

源码下载:MvcQuery.rar (VS2010 MVC3 项目,1758KB)

在线演示:http://asp-net-mvc-expression-trees-as-action-parameter.ldp.me

http://www.cnblogs.com/ldp615/archive/2011/09/16/asp-net-mvc-expression-trees-as-action-parameter.html

ASP.NET MVC:Expression Trees 作为参数简化查询的更多相关文章

  1. Expression Trees 参数简化查询

    ASP.NET MVC 引入了 ModelBinder 技术,让我们可以在 Action 中以强类型参数的形式接收 Request 中的数据,极大的方便了我们的编程,提高了生产力.在查询 Action ...

  2. 解决Win10系统下 C# DateTime 出现星期几的问题 解决ASP.NET MVC 接受Request Playload参数问题

    解决Win10系统下 C# DateTime 出现星期几的问题 昨天晚上写代码的时候偶然发现 DateTime 里出现了星期几,当时一阵凌乱,去网上百度没有详细解决办法,很多人说可以用用 ToStri ...

  3. ASP.NET MVC 5 学习教程:添加查询

    原文 ASP.NET MVC 5 学习教程:添加查询 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 通过控 ...

  4. 【译】ASP.NET MVC 5 教程 - 8:搜索查询

    原文:[译]ASP.NET MVC 5 教程 - 8:搜索查询 添加一个搜索的方法和搜索的视图 在本节中,我们为 Index 方法添加查询功能,使我们能够根据电影的题材或名称进行查找. 修改 Inde ...

  5. ASP.NET MVC 后台接收集合参数和 jquery ajax 传值

    MVC 接收参数数组(集合)   示例样本:   public class Person {      public string FirstName { get; set; }      publi ...

  6. ASP.NET MVC控制器Controller中参数

    前述文章参见:ASP.NET MVC控制器Controller 绪论 之前的控制器返回的均为常量字符串,接下来展示如何获取请求传来的参数,而返回"动态"的字符串. 可以在操作方法B ...

  7. ASP.NET MVC 给Action的参数赋值的方式

    Action指的是Controller类中的方法,如上文中的Index. Action参数的三种常见类型:Model类型.普通参数.FormCollection Model类型 我们可以直接在地址栏后 ...

  8. ASP.NET MVC利用ActionLink实现动态组合查询

    一个文件传输数据表,内有日期.传输协议.传输方向(上传或下载).文件名等信息,完整的表内容显示如下: 现在需要分类查询,即按照协议和传输方向查询.单独的分类查询问题并不大,比如,按协议查询,在View ...

  9. ASP.NET MVC post请求接收参数的三种方式

    1.在控制器中建立一个PostDemo方法,建立视图创建一个表单 <h2>PostDemo</h2> name的值:@ViewBag.name <br /> nam ...

随机推荐

  1. 基于Unity3D云人脸监測技术

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师.CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D ...

  2. ibatis 批量插入

      ibatis 批量插入 CreationTime--2018年7月2日10点21分 Author:Marydon 1.说明 基于oracle的sql语句 2.主键id有默认值,比如:sys_gui ...

  3. PHP-根据字符串和所用字体计算字符串所占宽高

    今天由于用GD画图, 需要把一段文字在一个框内居中, 但是文字的宽度如果用strlen($str) * $font_size来计算的话, 由于文字不是等宽高的, 所以会导致偏离, 最后选用的GD库的i ...

  4. js表单验证控制代码大全

    http://www.cnblogs.com/SAL2928/archive/2008/10/24/1319020.html目录: 1:js 字符串长度限制.判断字符长度.js限制输入.限制不能输入. ...

  5. 简述document.write和 innerHTML的区别。

    document.write是重写整个document, 写入内容是字符串的htmlinnerHTML是HTMLElement的属性,是一个元素的内部html内容

  6. sql 百分比

    select [city], bfb=cast(cast(count(*)*100./(select count(*) from [UserBasicInfo]) as decimal(10,0)) ...

  7. IOS性能调优系列:使用Allocation动态分析内存使用情况

    硬广:<IOS性能调优系列>第三篇,持续更新,欢迎关注. <IOS性能调优系列:Analyze静态分析>介绍了使用静态分析方法查找IOS内存泄漏的方法,<IOS性能调优系 ...

  8. SIM800L透传模式配置

    UART1_SendString("AT+CIPCLOSE=1"); //关闭连接 delay_ms(100); Second_AT_Command("AT+CIPSHU ...

  9. 【Android】14.0 第14章 内部存储与外部SD卡存储—本章示例主界面

    分类:C#.Android.VS2015: 创建日期:2016-02-27 一.简介 Android使用的文件系统是基于Linux的文件系统,在Android应用程序中,开发人员既可以建立和访问程序自 ...

  10. 关于 initWithNibName 和 loadNibNamed 的区别和联系-iPhone成长之路

    转自:http://blog.sina.com.cn/s/blog_7b9d64af01018f2u.html 关于 initWithNibName 和 loadNibNamed 的区别和联系.之所以 ...