ASP.NET MVC:Expression Trees 作为参数简化查询 二
2011-09-18 17:25 鹤冲天 阅读(5795) 评论(15) 编辑 收藏 举报前文《ASP.NET MVC:Expression Trees 作为参数简化查询》中提出可以将 Expression Trees 用作查询 Action 的参数来简化编码:
1 2 3 4 |
public ActionResult Index([QueryConditionBinder]Expression<Func<Employee, bool>> predicate) { var employees = repository.Query().Where(predicate); return View("Index", employees); } |
本文的内容稍有枯燥,先给出最终的运行截图,给大家提提神:
演示网站运行截图
在线演示:http://demos.ldp.me/employees
下图显示的 Expression 是根据查询条件动态生成的:
调试截图:
设计目标
支持以下类型查询:
- 相等查询
- 字符串查询:完全匹配、模糊查询、作为开始、作为结束;
- 日期查询(不考虑时间)、日期范围查询;
- 比较查询:大于、大于等于、小于、小于等于;
- …
- 正确处理可空类型
阻止某些查询:
- 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 OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<IdKeyDiscoveryConvention>(); } |
不错吧,但 EF 不允许添加新的 Convention,有点遗憾。
分解出 Convention
借鉴 EF 的思路,我们可以分解出以下 Convention:
- ValueTypeEqualsConvention:值类型相等,年龄 == 18、婚否 = false;
- 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<string, Expression> 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<string, Expression>(); var propertyExpressionConvertions = _conventionConfiguration.GetConventions<IPropertyExpressionConvention>(); 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<IExpressionCombineConvention>()) { body = convention.Combine(dict); if (body != null) break; } //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; } } |
高亮代码为修改或新增部分。
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<IConvention>(); } public HashSet<IConvention> Conventions { get; private set; } internal IEnumerable<T> GetConventions<T>() where T: IConvention { return Conventions .OfType<T>() .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<string, System.Linq.Expressions.Expression> 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。
项目类图
目前实现中主要有以下类和接口:
扩展方法类未列出。
QueryConditionExpressionModelBinder 使用
直接使用
1 2 3 4 |
public ActionResult Index([QueryConditionBinder]Expression<Func<Employee, bool>> 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<Func<Order, bool>>), new QueryConditionExpressionModelBinder(cfg)); |
1 2 3 4 5 6 7 |
public class OrdersController : Controller{ private OrdersRepository repository = new OrdersRepository(); public ViewResult Index(Expression<Func<Order, bool>> predicate) { var orders = repository.Query().Where(predicate); return View(orders); } } |
后记
根据你的项目,创建适合的 Convention,相信 QueryConditionExpressionModelBinder 一定会帮你省下很多时间。
本文中代码编写仓促,尚未进行严格测试,使用时请注意。如有 bug 请回复给我,谢谢!
后续还有相关文章,实现禁止对某些属性查询的 Convention,以及复杂条件组合 Convention 等等。
源码下载:MvcQuery2.rar (1733KB,VS2010 MVC3)
-------------------
思想火花,照亮世界