ASP.NET MVC 引入了 ModelBinder 技术,让我们可以在 Action 中以强类型参数的形式接收 Request 中的数据,极大的方便了我们的编程,提高了生产力。在查询 Action 中,我们可以将 Expression Trees 用作参数,通过自定义的 ModelBinder 动态自动构建查询表达式树,进一步发挥 MVC 的威力,简化编码工作。
MVC 查询和存在的不足
下面是一个查询 Employee 的 Action,在 MVC 项目中经常可以见到:
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 判断,以致代码行数比较多,不是特别清晰。
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 Trees 作为查询 Action的参数来弥补这些不足。
使用 Expression<Func<T, bool>> 作为 Action 的参数
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 获取。
下面给出一个粗略实现,仅用来说明这个思路是可行的:
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;} }
如果不想在 Global.asax 文件中设置 Expression<Func<T, bool>> 的 ModelBinder, 可以借助用下面这个 Attribute 类:
public class QueryConditionBinderAttribute : CustomModelBinderAttribute {public override IModelBinder GetBinder() {return new QueryConditionExpressionModelBinder();} }
Index3 简单修改如下:
public ActionResult Index3([QueryConditionBinder]Expression<Func<Employee, bool>> predicate) { //... }