引言
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 项目中也是这样处理的。
但时间一长,我逐步也发现了这种方式一些不足之处:
- 首先,网站中有很多类似的查询,如Customer、Order、Product 等等。而且大致也有点规律:字符串的一般模糊查询,时间日期类的一般按日期查询(忽略时间),其它类型则相等查询。不同 Model 查询的 Action 编码总有八、九分相似,但又不是简单的重复,却又难以重构。
- 需求变动,如增加一个查询条件,修改 View 是必须的,但也要修改 Action,增加一个参数,还要加一行 Where 或 WhereIf。多处修改,烦人啊,而且这种需求变动又是比较频繁的,尤其是在项目初期。若能只修改 View 而不修改 Action 就爽了。
- …
使用 Expression> 作为 Action 的参数
试看如下代码:1 2 3 4
public ActionResult Index3(Expression
我将 Expression Trees 作为 Action 的唯一的参数(暂不考虑分页、排序等),将所有的查询条件都统一汇集至 predicate 参数。
所有的查询(不管是 Employee 还是 Customer)都使用如上代码。其它实体查询只需修改参数的类型,如 Customer 查询改为 Expression
细心品味下,相信你能理解这种做法的精妙之处!
如上修改代码后,直接运行会报错,因为 MVC 中默认的数据绑定器 DefaultModelBinder 不能正确绑定 Expression
我们要新创一个新的 ModelBinder。
创建 QueryConditionExpressionModelBinder
我们需要一个新的 ModelBinder 来为 ExpressionQueryConditionExpressionModelBinder 要根据上下文来自动生成查询的 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); } ///
了解这段代码,需要 MVC 和 Expression Trees 的一些知识。这段代码还用到了 Expression 扩展方法,参见:《c# 扩展方法奇思妙用基础篇九:Expression 扩展》。
如果不想在 Global.asax 文件中设置 Expression
1 2 3 4 5
public class QueryConditionBinderAttribute : CustomModelBinderAttribute { public override IModelBinder GetBinder() { return new QueryConditionExpressionModelBinder(); } }
Index3 简单修改如下:
1
public ActionResult Index3([QueryConditionBinder]Expression
下面是一个调试截图,绑定正常。
再次说明:本部分代码仅用来说明思路可行,用了大量的硬编码。
我也正在准备编写一个更加灵活 QueryConditionExpressionModelBinder,来应对复杂的查询(如时间范围、值大于、小于等、以及限制对某些属性的查询),目前也有了一个大体的思路,初步完成后在之后的博文中和大家分享下。如果你有好的思路,不妨写在回复中。
源码下载:MvcQuery.rar (VS2010项目,1758KB)
将 MVC 发挥的极致,是我的最求!
最新评论