1 2 3 4
public ActionResult Index([QueryConditionBinder]Expression
文中给出的 QueryConditionExpressionModelBinder 类,比较僵化,无法满足实际要求。本文将会从这个类为起点,构建一个灵活的解决方案。
本文的内容稍有枯燥,先给出最终的运行截图,给大家提提神:
演示网站运行截图
在线演示:http://demos.ldp.me/employees下图显示的 Expression 是根据查询条件动态生成的:
调试截图:
设计目标
支持以下类型查询:- 相等查询
- 字符串查询:完全匹配、模糊查询、作为开始、作为结束;
- 日期查询(不考虑时间)、日期范围查询;
- 比较查询:大于、大于等于、小于、小于等于;
- …
- 正确处理可空类型
- ID查询
- 某些保密属性,如内部价格属性等
- 系统容易扩展,开放支持加入新的查询类型
- 简单使用
- 查询数据验证,配合 MVC 相应机制,对错误输入给出提示。
思考
想法源自 Entity Framework:EF 中的 Convention
在 EF Code First 中,Entity 与 数据库 Table 之间映射采用 Convention (约定) 的方式:- PluralizingTableNameConvention:实体使用单数形式,自动对应数据库中复数形式的表名;
- IdKeyDiscoveryConvention:自动找寻主键,名为 Id 或 Entity 类名 + Id 的属性自动认为是主键;
如果你认可其中的某条 Convention 你可以将它移除:
1 2 3 4 5
public class NorthwindDbContext : DbContext { protected override void _disibledevent=>
- AndCombineConvention:并且,在页面查询中,这个比较常用,我们设成默认的;
- OrCombinedConvention:或者;
- XXXComplexCombineConvention:更加复杂的情况,如:(存款 > 100,000,000) Or ((年龄 <= 18) 并且 (婚否 = false))。
可设置的 Order 属性
给每个 Convention 设置一个优先顺序号,大的优先级高:- StringContainsConvention、DateEqualsConvention 优先于 ValueTypeEqualsConvention;
- BetweenDatesConvention 优先于 DateEqualsConvention。
编码时会根据实际应用给每个 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(IDictionaryexpressions); }
修改 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 76public 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
高亮代码为修改或新增部分。
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
实现具体 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(IDictionaryexpressions) { if(expressions.Count > 0) return expressions.Values.Aggregate((a, e) => a.OrElse(e)); return null; } }
项目类图
目前实现中主要有以下类和接口:扩展方法类未列出。
QueryConditionExpressionModelBinder 使用
直接使用
1 2 3 4public ActionResult Index([QueryConditionBinder]Expression
或配置后使用
若你有新创建的 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
这时,就不要再使用 QueryConditionBinderAttribute 了:
1 2 3 4 5 6 7
public class OrdersController : Controller{ private OrdersRepository repository = new OrdersRepository(); public ViewResult Index(Expression
后记
根据你的项目,创建适合的 Convention,相信 QueryConditionExpressionModelBinder 一定会帮你省下很多时间。本文中代码编写仓促,尚未进行严格测试,使用时请注意。如有 bug 请回复给我,谢谢!
后续还有相关文章,实现禁止对某些属性查询的 Convention,以及复杂条件组合 Convention 等等。
源码下载:MvcQuery2.rar (1733KB,VS2010 MVC3)
在线演示:http://demos.ldp.me/employees
最新评论