zoukankan      html  css  js  c++  java
  • ASP.NET MVC:Expression Trees 作为参数简化查询

    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 项目中也是这样处理的。

    但时间一长,我逐步也发现了这种方式一些不足之处:

    1. 首先,网站中有很多类似的查询,如Customer、Order、Product 等等。而且大致也有点规律:字符串的一般模糊查询,时间日期类的一般按日期查询(忽略时间),其它类型则相等查询。不同 Model 查询的 Action 编码总有八、九分相似,但又不是简单的重复,却又难以重构
    2. 需求变动,如增加一个查询条件,修改 View 是必须的,但也要修改 Action,增加一个参数,还要加一行 Where 或 WhereIf。简单变动却多处修改,烦人啊,而且这种需求变动又是比较频繁的,尤其是在项目初期。若能只修改 View 而不修改 Action 就爽了。

    思考后,我决定使用 Expression Trees 作为查询 Action的参数来弥补这些不足。

    使用 Expression<Func<T, bool>> 作为 Action 的参数

    试看如下代码:

    1
    2
    3
    4
    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 获取。

    下面给出一个粗略实现,仅用来说明这个思路是可行的:

    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);
        }
        /// <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;
        }
    }

    了解这段代码,需要 MVC 和 Expression Trees 的一些知识。这段代码还用到了 Expression 扩展方法,参见:《c# 扩展方法奇思妙用基础篇九:Expression 扩展》。

    如果不想在 Global.asax 文件中设置 Expression<Func<T, bool>> 的 ModelBinder, 可以借助用下面这个 Attribute 类:

    1
    2
    3
    4
    5
    public class QueryConditionBinderAttribute : CustomModelBinderAttribute {
        public override IModelBinder GetBinder() {
            return new QueryConditionExpressionModelBinder();
        }
    }

    Index3 简单修改如下:

    1
    public ActionResult Index3([QueryConditionBinder]Expression<Func<Employee, bool>> predicate) { //... }

    下面是一个调试截图,绑定正常。

    image

    再次说明:本部分代码仅用来说明思路可行,用了大量的硬编码。

    我也正在准备编写一个更加灵活 QueryConditionExpressionModelBinder,来应对复杂的查询(如时间范围、值大于、小于等、以及限制对某些属性的查询),目前也有了一个大体的思路,初步完成后在之后的博文中和大家分享下。如果你有好的思路,不妨写在回复中。

    源码下载:MvcQuery.rar (VS2010 MVC3 项目,1758KB)

    在线演示:http://asp-net-mvc-expression-trees-as-action-parameter.ldp.me

     http://www.cnblogs.com/ldp615/archive/2011/09/16/asp-net-mvc-expression-trees-as-action-parameter.html

  • 相关阅读:
    网上购物瘾,你怎么能退出?
    POJ 1006 Biorhythms 中国的法律来解决剩余的正式
    【Android接口实现】PhotoView——单点支持/多图像缩放,实现了触摸
    线程同步synchronized
    阿里云CentOS 6.5 设备、执行Docker容器和步骤的方法
    打破了中国电信华为无线路由猫(HG522-C)自己主动拨号+任意数量的计算机+iTV
    GCC 命令行具体解释
    Nginx 负载均衡
    Linux pipe功能
    Java有用的经验--Swing片
  • 原文地址:https://www.cnblogs.com/sjqq/p/8649578.html
Copyright © 2011-2022 走看看