trees,ASP.NET MVC:Expression Trees 作为参数简化查询 二

前文《ASP.NET MVC:Expression Trees 作为参数简化查询》中提出可以将 Expression Trees 用作查询 Action 的参数来简化编码:
1 2 3 4
public ActionResult Index([QueryConditionBinder]Expression> predicate) { var employees = repository.Query().Where(predicate); return View("Index", employees); }
文中给出的 QueryConditionExpressionModelBinder 类,比较僵化,无法满足实际要求。本文将会从这个类为起点,构建一个灵活的解决方案。
本文的内容稍有枯燥,先给出最终的运行截图,给大家提提神:

演示网站运行截图

在线演示:http://demos.ldp.me/employees
下图显示的 Expression 是根据查询条件动态生成的:
imageASP.NET MVC:Expression Trees 作为参数简化查询 二trees
调试截图:
imageimageASP.NET MVC:Expression Trees 作为参数简化查询 二trees

设计目标

支持以下类型查询:
  • 相等查询
  • 字符串查询:完全匹配、模糊查询、作为开始、作为结束;
  • 日期查询(不考虑时间)、日期范围查询;
  • 比较查询:大于、大于等于、小于、小于等于;
  • 正确处理可空类型
阻止某些查询:
  • ID查询
  • 某些保密属性,如内部价格属性等
扩展性:
  • 系统容易扩展,开放支持加入新的查询类型
易用性:
  • 简单使用
其它:
  • 查询数据验证,配合 MVC 相应机制,对错误输入给出提示。

思考

想法源自 Entity Framework:

EF 中的 Convention

在 EF Code First 中,Entity 与 数据库 Table 之间映射采用 Convention (约定) 的方式:
  • PluralizingTableNameConvention:实体使用单数形式,自动对应数据库中复数形式的表名;
  • IdKeyDiscoveryConvention:自动找寻主键,名为 Id 或 Entity 类名 + Id 的属性自动认为是主键;
 System.Data.Entity.ModelConfiguration.Conventions 命名空间中有很多这样的 Convention。这些 Convention 都是被大多人公认的,EF 运行时会加载这些 Convention,因此我们使用 EF 会相当简单,不需要像 NH 那样进行大量繁琐无聊的映射配置工作。
如果你认可其中的某条 Convention 你可以将它移除:
1 2 3 4 5
public class NorthwindDbContext : DbContext { protected override void _disibledevent=>
  • StringContainsConvention:字符串包含,即模糊查询;
  • DateEqualsConvention:日期等于,忽略时间;
  • ValueTypeCompareConvention:值类型比较,价格大于 12.00;
  • BetweenDatesConvention:时间界于两个日期之间;
  • IDForbiddenConvention:禁止对 ID 查询。
  • 还有一点,要将各个条件组合起来,如:(年龄 <= 18) 并且 (婚否 = false), 或者 (年龄 <= 18) 或者 (婚否 = false)。因此,还要定义用于连接组合的 Convention:
    • AndCombineConvention:并且,在页面查询中,这个比较常用,我们设成默认的;
    • OrCombinedConvention:或者;
    • XXXComplexCombineConvention:更加复杂的情况,如:(存款 > 100,000,000) Or ((年龄 <= 18) 并且 (婚否 = false))。

    可设置的 Order 属性

    给每个 Convention 设置一个优先顺序号,大的优先级高:
    • StringContainsConvention、DateEqualsConvention 优先于 ValueTypeEqualsConvention;
    • BetweenDatesConvention 优先于 DateEqualsConvention。
    即采用了 StringContainsConvention 就不会再采用 ValueTypeEqualsConvention。
    编码时会根据实际应用给每个 Convention 设置一个默认的合理的 Order 值,但为了灵活通用,允许修改,Order 是一个 get-set 属性。

    可以添加新的 Convention 以满足更多应用

    EF 只能移除不能添加,有时感不方便,不太符合 OCP(Open-Closed principle)。

    编码实现

    抽象出接口

    根据上面的分析,可以提取出下面三个接口:
    • IConvention 接口,代表所有的约定:
      1 2 3
      public interface IConvention { int Order { get; set; } }
    • IPropertyExpressionConvention 接口,将单个查询条件转换为 Expression:
      1 2 3
      public interface IPropertyExpressionConvention: IConvention { Expression BuildExpression(BuildPropertyExpressionContext context); }
    • IExpressionCombineConvention 接口,将多个查询 Expression 进行合并:
      1 2 3
      public interface IExpressionCombineConvention : IConvention { Expression Combine(IDictionary expressions); }

    修改 QueryConditionExpressionModelBinder 类

    修改后代码如下: 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 73 74 75 76
    public class QueryConditionExpressionModelBinder : IModelBinder { private ConventionConfiguration _conventionConfiguration; public QueryConditionExpressionModelBinder(ConventionConfiguration conventionConfiguration) { _conventionConfiguration = conventionConfiguration; } public QueryConditionExpressionModelBinder(): this(ConventionConfiguration.Default) { } public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var modelType = GetModelTypeFromExpressionType(bindingContext.ModelType); if (modelType == null) return null; var parameter = Expression.Parameter(modelType, modelType.Name[0].ToString().ToLower()); var dict = new Dictionary(); var propertyExpressionConvertions = _conventionConfiguration.GetConventions(); foreach (var property in modelType.GetProperties()){ foreach (var convention in propertyExpressionConvertions) { var context = new BuildPropertyExpressionContext( property, bindingContext.ValueProvider, controllerContext.Controller.ViewData.ModelState, parameter.Property(property.Name) ); var expression = convention.BuildExpression(context); if(expression != null){ dict.Add(property.Name, expression); break; } if (context.IsHandled) break; } } var body = default(Expression); foreach (var convention in _conventionConfiguration.GetConventions()) { body = convention.Combine(dict); if (body != null) break; } //if (body == null) body = Expression.Constant(true); return body.ToLambda(parameter); } /// /// 获取 Expression> 中 TXXX 的类型 /// 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]; } /// /// 获取属性的查询值并处理 Controller.ModelState /// 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; } }

    高亮代码为修改或新增部分。
    QueryConditionExpressionModelBinder 中使用了 ConventionConfiguration 类:
    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
    public class ConventionConfiguration { public static ConventionConfiguration Default = new ConventionConfiguration(); static ConventionConfiguration() { Default.Conventions.Add(new ValueTypeEqualsConvention()); Default.Conventions.Add(new StringContainsConvention()); Default.Conventions.Add(new DateEqualsConvention()); Default.Conventions.Add(new BetweenDatesConvention()); // Default.Conventions.Add(new AwalysTrueCombineConvention()); Default.Conventions.Add(new OrCombineConvention()); } public ConventionConfiguration() { Conventions = new HashSet(); } public HashSet Conventions { get; private set; } internal IEnumerable GetConventions() where T: IConvention { return Conventions .OfType() .OrderByDescending(c => c.Order); } }

    实现具体 Converntion:

    • ValueTypeEqualsConvention 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      public class ValueTypeEqualsConvention : PropertyExpressionConventionBase { public ValueTypeEqualsConvention():base(1) {} public override Expression BuildExpression(BuildPropertyExpressionContext context) { if (!context.Property.PropertyType.IsValueType) return null; var queryValue = context.ValueProvider.GetQueryValue(context.Property.Name, context.Property.PropertyType); context.ModelState.AddIfValueNotNull(context.Property.Name, queryValue.ModelState); context.IsHandled = queryValue.ModelState != null; if(queryValue.Value == null) return null; return context.PropertyExpression.Equal(Expression.Constant(queryValue.Value)); } }
    • StringContainsConvention 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
      public class StringContainsConvention : PropertyExpressionConventionBase { public StringContainsConvention():base(10) { } public override Expression BuildExpression(BuildPropertyExpressionContext context) { if (context.Property.PropertyType != typeof(string)) return null; var queryValue = context.ValueProvider.GetQueryValue(context.Property.Name, context.Property.PropertyType); context.ModelState.AddIfValueNotNull(context.Property.Name, queryValue.ModelState); context.IsHandled = queryValue.ModelState != null; if ((queryValue.Value as string).IsNullOrEmpty()) return null; return context.PropertyExpression.Call("Contains", Expression.Constant(queryValue.Value)); } }
    • DateEqualsConvention 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
      public class DateEqualsConvention: PropertyExpressionConventionBase { public DateEqualsConvention():base(10) { } public override System.Linq.Expressions.Expression BuildExpression(BuildPropertyExpressionContext context) { if (context.Property.PropertyType.NotIn(typeof(DateTime), typeof(DateTime?))) return null; if (!context.Property.Name.EndsWith("day", true, CultureInfo.CurrentCulture) && !context.Property.Name.EndsWith("date", true, CultureInfo.CurrentCulture)) return null; var queryValue = context.ValueProvider.GetQueryValue(context.Property.Name, typeof(DateTime)); context.ModelState.AddIfValueNotNull(context.Property.Name, queryValue.ModelState); context.IsHandled = queryValue.ModelState != null; if (queryValue.Value == null) return null; var date = ((DateTime)queryValue.Value).Date; var expression = context.PropertyExpression; if (expression.Type == typeof(DateTime?)) expression = expression.Property("Value"); return expression.Property("Date").Equal(Expression.Constant(date)); } }
    • AndCombineConvention 1 2 3 4 5 6 7 8
      public class AndCombineConvention : IExpressionCombineConvention { public int Order { get; set; } public System.Linq.Expressions.Expression Combine(IDictionary expressions) { if(expressions.Count > 0) return expressions.Values.Aggregate((a, e) => a.OrElse(e)); return null; } }
    特别注意下 DateEqualsConvention,只对名称以 day 或 date 结尾(不区分大小)的 DateTime 或 DateTime?属性进行处理,如 Employee.Birthday、Employee.HireDate。

    项目类图

    目前实现中主要有以下类和接口:
    imageimageimageASP.NET MVC:Expression Trees 作为参数简化查询 二trees    imageimageimageimageASP.NET MVC:Expression Trees 作为参数简化查询 二trees
    imageimageimageimageimageASP.NET MVC:Expression Trees 作为参数简化查询 二trees    imageimageimageimageimageimageASP.NET MVC:Expression Trees 作为参数简化查询 二trees
    扩展方法类未列出。

    QueryConditionExpressionModelBinder 使用

    直接使用

    1 2 3 4
    public ActionResult Index([QueryConditionBinder]Expression> predicate) { var employees = repository.Query().Where(predicate); return View("Index", employees); }

    或配置后使用

    若你有新创建的 Convention,可以在 Global.asax 文件中 MvcApplication.Application_Start 方法中进行加入配置:
    1
    ConventionConfiguration.Default.Conventions.Add(new YourConvention());
    如果默认的 Conversions 不满足你的要示,可以移除后重新增加:
    1 2 3 4
    ConventionConfiguration.Default.Conventions.Clear(); ConventionConfiguration.Default.Conventions.Add(new ValueTypeEqualsConvention()); ConventionConfiguration.Default.Conventions.Add(new DateEqualsConvention { Order = 1000 }); ConventionConfiguration.Default.Conventions.Add(new YourConvention{ Order = 2000});
    因为 Order 属性是可修改的,添加时可以重新指定优先级。
    或都你可以给某一个查询单独配置 Convention: 1 2 3
    var cfg = new ConventionConfiguration(); cfg.Conventions.Add(new StringContainsConvention()); ModelBinders.Binders.Add(typeof(Expression>), new QueryConditionExpressionModelBinder(cfg));
    这时,就不要再使用 QueryConditionBinderAttribute 了:
    1 2 3 4 5 6 7
    public class OrdersController : Controller{ private OrdersRepository repository = new OrdersRepository(); public ViewResult Index(Expression> predicate) { var orders = repository.Query().Where(predicate); return View(orders); } }

    后记

    根据你的项目,创建适合的 Convention,相信 QueryConditionExpressionModelBinder 一定会帮你省下很多时间。
    本文中代码编写仓促,尚未进行严格测试,使用时请注意。如有 bug 请回复给我,谢谢!
    后续还有相关文章,实现禁止对某些属性查询的 Convention,以及复杂条件组合 Convention 等等。
     
    源码下载:MvcQuery2.rar (1733KB,VS2010 MVC3)
    在线演示:http://demos.ldp.me/employees
    Tags: 

    延伸阅读

    最新评论

    发表评论