zoukankan      html  css  js  c++  java
  • 浅析ASP.net Web API的Model验证(使用MVC4框架的Web API须谨慎)

    ASP.net Web API的验证器有三个,可以通过下面的代码看出来:

            public static void Register(HttpConfiguration config)
            {
                //...
    
                var test = config.Services.GetModelValidatorProviders();
                foreach (var val in test)
                {
                    System.Diagnostics.Debug.WriteLine(val.GetType());
                }
            }

    上面代码会在Output窗口打印出:
    System.Web.Http.Validation.Providers.DataAnnotationsModelValidatorProvider
    System.Web.Http.Validation.Providers.DataMemberModelValidatorProvider
    System.Web.Http.Validation.Providers.InvalidModelValidatorProvider

    这是默认的情况,这三个验证器的作用是不同的,下面是来自MSDN的描述:

    • DataAnnotationsModelValidatorProvider - Represents an implementation of ModelValidatorProvider which providers validators for attributes which derive from ValidationAttribute. It also provides a validator for types which implement IValidatableObject. To support client side validation, you can either register adapters through the static methods on this class, or by having your validation attributes implement IClientValidatable. The logic to support IClientValidatable is implemented in DataAnnotationsModelValidator 根据这段描述,我认为它相当于MVC的验证器。(事实上还是不尽相同)
    • DataMemberModelValidatorProvider - Represents a validator provider for data member model. MSDN的描述太粗略了,并且没有代码说明,我不太明白这个干嘛用的。
    • InvalidModelValidatorProvider - An implementation of ModelValidatorProvider which provides validators that throw exceptions when the model is invalid. 简单说就是用来抛出异常的。

    根据MVC的传统做法,不管用户传入参数有什么错或漏,都不会抛出异常,这种错漏应该只会体现在ModelState中,所以InvalidModelValidatorProvider被认为是个不太好的设计。屏蔽掉它的方法是:

    config.Services.RemoveAll(typeof(System.Web.Http.Validation.ModelValidatorProvider),v => v is InvalidModelValidatorProvider);

    上面仅仅是“想当然”的做法(由于缺乏相关资料,得知上述的这些信息居然还花了我很多时间),而实际上则非常怪异,我很怀疑目前伴随MVC4发布的这套Web API框架的设计有较大的缺陷。OK,接下来再具体一点点分析。

    首先我们用MVC4的向导创建一个默认的Web API项目,再创建一个非常传统的Model——Order(订单)

        public class Order_UI : IValidatableObject
        {
            public string ModelType { get; set; } //"Add" "Edit"
    
            [DisplayName("订单号")]
            [RequiredIf("ModelType", "Edit", "订单号不可为空")]
            public int? OrderId { get; set; }
    
            [DisplayName("客户名称")]
            [Required(ErrorMessage = "客户名称不可为空")]
            [RegularExpression(NJT.Comm.Verifier.REG_EXP_USER_NAME, ErrorMessage = NJT.Comm.Verifier.ERRMSG_REG_EXP_USER_NAME)]
            public string CustomerName { get; set; }
    
            [DisplayName("订单提交日期")]
            [Required(ErrorMessage = "订单提交日期不可为空")]
            public DateTime OrderDate { get; set; } //即便是不可空类型,也需要加上Required,否则Formatter会自动给它带上一个默认值
    
            [DisplayName("运费")]
            [Required(ErrorMessage = "运费不可为空")]
            public decimal Freight { get; set; }
    
            [DisplayName("总金额")]
            [Required(ErrorMessage = "总金额不可为空")]
            public decimal Amount { get; set; }
    
            public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
            {
                if (this.ModelType != "Add" && this.ModelType != "Edit")
                    yield return new ValidationResult("ModelType必须为Add或Edit");
            }
        }

    我的意思很明确,除了订单Id在Add的时候可以不提供外,其它字段都必须提供,否则返回相关出错的信息。关于自定义的RequiredIf验证,可以参考这个网址:http://mvcvalidatortoolkit.codeplex.com

    BTW:Required特性放在decimal和DateTime类型的属性上并不是必须的,因为decimal和DateTime本来就不可为空,我之所以这样写完全是为了一个自定义的ErrorMessage,在传统MVC网站中,这种做法一点问题都没有。

    ApiController中:

            public void Post([FromBody]Order value)
            {
                //...
            }

    我们使用Finddler作为测试客户端观察一下究竟发生什么事情。

    POST
    ----Header----
    Content-type: application/json
    ----Content----
    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12,"Amount":50.5}

    结果出现了异常:

    [System.InvalidOperationException]
    {"Property 'OrderDate' on type 'IntegrationDemo.Models.Order' is invalid. Value-typed properties marked as [Required] must also be marked with [DataMember(IsRequired=true)] to be recognized as required. Consider attributing the declaring type with [DataContract] and the property with [DataMember(IsRequired=true)]."}

    很明显,它说OrderDate这种数值类型的属性,如果带上了[Required]的话,就得再带上[DataMember(IsRequired=true)],这是什么道理呢?我想这是因为Web API的Formatter需要知道如何去反序列化,而DataMember这种特性,就是用来指导序列化/反序列化的,这点我并没有太多深究,这里只是大概一下。

    要处理这个问题,有三种方法,一是本文开头说的那样,把InvalidModelValidatorProvider验证器拿掉,另一种做法是去掉[Required]特性(嗯,自行妥协下),第三种做法就是照着异常的提示去做。我们先尝试第一种做法。

    一、把InvalidModelValidatorProvider去掉

    这次再Finddler一下,ModelState通过了!但,别高兴得太早,接下来一大堆问题。

    我先把POST的内容改为: {"ModelType":"Add","OrderDate":"2012-10-08","Freight":1.12,"Amount":50.5}

    也就是去掉CustomerName,这次验证没通过,观察ModelState,有两个ModelError,一个是:

    Errors[0].ErrorMessage = ""
    Errors[0].Exception = {"Required property 'CustomerName' not found in JSON. Path '', line 1, position 73."}

    另一个是:

    Errors[0].ErrorMessage = "客户名称不可为空"
    Errors[0].Exception = null

    这看起来很不优雅,一个是Exception,这个Exception应该是Json序列化器抛出并存放入ModelState中去的;另一个是ErrorMessage,这个应该是DataAnnotationsModelValidatorProvider这个验证器检验的结果。其实我们想要的只是“客户名称不可为空”这个ErrorMessage,而不是那个Exception,能否关掉那个Json序列化器的异常?我现在不知道怎么弄。

    OK,再动一下POST的内容,改为:

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12}

    这次是把Amount拿掉,你认为跟刚才的结果类似吗?想想看,我以为类似,但实际上并不类似,观察ModelState,这次只有一个ModelError,是:

    Errors[0].ErrorMessage = ""
    Errors[0].Exception = {"Required property 'Amount' not found in JSON. Path '', line 1, position 84."}

    这次错误信息中看不到我们自定义的ErrorMessage了,看来对于Amount这种数值类型的属性,DataAnnotationsModelValidatorProvider并不验证。

    如果你把POST的内容改为:

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12,"Amount":"abc"}

    会在ModelState中观察到两个Exception,一个是:

    Errors[0].ErrorMessage = ""
    Errors[0].Exception = {"Could not convert string to decimal: abc. Path 'Amount', line 1, position 98."}

    另一个是:

    Errors[0].ErrorMessage = ""
    Errors[0].Exception = {"Required property 'Amount' not found in JSON. Path '', line 1, position 99."}

    总而言之,很不统一,想返回一个比较友好的出错信息有难度。

    二、把[Required]拿掉

    好,我们来尝试第二种方案,验证器先恢复原来的设置,然后把所有数值类型属性(OrderDate、Freight和Amount)的[Required]特性拿掉。

    尝试这个:

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12,"Amount":50.5}

    OK,通过了,尝试一下别的:

    {"ModelType":"Add","CustomerName":"Guogang"}

    结果竟然也能通过!很显然,我们现在要马上否决掉这种方案。

    三、听它的话

    给数值类型的属性加上[DataMember(IsRequired=true)]特性,需要引入“System.Runtime.Serialization”,还需要用[DataContract]来修饰Order。这样一来,Model就变成了:

        [DataContract]
        public class Order : IValidatableObject
        {
            public string ModelType { get; set; } //"Add" "Edit"
    
            [DisplayName("订单号")]
            [RequiredIf("ModelType", "Edit", "订单号不可为空")]
            public int? OrderId { get; set; }
    
            [DisplayName("客户名称")]
            [Required(ErrorMessage = "客户名称不可为空")]
            [RegularExpression(NJT.Comm.Verifier.REG_EXP_USER_NAME, ErrorMessage = NJT.Comm.Verifier.ERRMSG_REG_EXP_USER_NAME)]
            public string CustomerName { get; set; }
    
            [DisplayName("订单提交日期")]
            [Required(ErrorMessage = "订单提交日期不可为空")]
            [DataMember(IsRequired=true)]
            public DateTime OrderDate { get; set; } //即便是不可空类型,也需要加上Required,否则Formatter会自动给它带上一个默认值
    
            [DisplayName("运费")]
            [Required(ErrorMessage = "运费不可为空")]
            [DataMember(IsRequired = true)]
            public decimal Freight { get; set; }
    
            [DisplayName("总金额")]
            [Required(ErrorMessage = "总金额不可为空")]
            [DataMember(IsRequired = true)]
            public decimal Amount { get; set; }
    
            public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
            {
                if (this.ModelType != "Add" && this.ModelType != "Edit")
                    yield return new ValidationResult("ModelType必须为Add或Edit");
            }
        }

    尝试这个:

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12,"Amount":50.5}

    和前两次尝试不同,这次没通过,出现了一个ErrorMessage:

    Errors[0].ErrorMessage = "客户名称不可为空"
    Errors[0].Exception = null

    有冇搞错?我有提供"CustomerName"啊!

    经过分析:由于使用了数据契约(DataContract)特性,需要序列化/反序列化的的成员都必须带上DataMember特性。我们把Order这个Model再改一改:

        [DataContract]
        public class Order : IValidatableObject
        {
            [DataMember]
            public string ModelType { get; set; } //"Add" "Edit"
    
            [DisplayName("订单号")]
            [RequiredIf("ModelType", "Edit", "订单号不可为空")]
            [DataMember]
            public int? OrderId { get; set; }
    
            [DisplayName("客户名称")]
            [Required(ErrorMessage = "客户名称不可为空")]
            [DataMember(IsRequired = true)]
            [RegularExpression(NJT.Comm.Verifier.REG_EXP_USER_NAME, ErrorMessage = NJT.Comm.Verifier.ERRMSG_REG_EXP_USER_NAME)]
            public string CustomerName { get; set; }
    
            [DisplayName("订单提交日期")]
            [Required(ErrorMessage = "订单提交日期不可为空")]
            [DataMember(IsRequired=true)]
            public DateTime OrderDate { get; set; } //即便是不可空类型,也需要加上Required,否则Formatter会自动给它带上一个默认值
    
            [DisplayName("运费")]
            [Required(ErrorMessage = "运费不可为空")]
            [DataMember(IsRequired = true)]
            public decimal Freight { get; set; }
    
            [DisplayName("总金额")]
            [Required(ErrorMessage = "总金额不可为空")]
            [DataMember(IsRequired = true)]
            public decimal Amount { get; set; }
    
            public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
            {
                if (this.ModelType != "Add" && this.ModelType != "Edit")
                    yield return new ValidationResult("ModelType必须为Add或Edit");
            }
        }

    再试一试,这次通过了!

    尝试一些错漏的情况:

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12,"Amount":"abc"}

    会出现一个转换异常和一个缺失异常

    {"ModelType":"Add","CustomerName":"Guogang","OrderDate":"2012-10-08","Freight":1.12}

    会出现一个缺失异常

    我还测试了其它一些情况,用下来发觉跟第一种方案很类似,看不出什么差别。

    总结

    总结回来,第一种方案和第三种方案都是可取的,第一种方案简单,第三种方案繁琐些,但却提供了额外的序列化选项。那接下来的问题是:如何给客户端返回一个比较友好的请求参数错误信息?

    我看很有难度,从上面的例子可以看出,对于带[Required]的数值类型属性,如果没有被提供,那么只会出现序列化异常,而不会有ErrorMessage,而对于string,既有序列化异常,也有ErrorMessage。我这里提供一个权宜之计:如果有序列化异常的话,向客户端简单地返回一个出错信息:请求参数缺失或错误;如果没有序列化异常,则返回ErrorMessage集合。

    这样能用,但距离完美差太远了。关于Web API的ModelState的吐槽,我还有很多,我甚至到微软的网站去问过,无奈获取到的信息还是太少,而直接修改MVC4的源代码对我来说不太有把握,也担心一旦微软发布了新的版本,自己的修改也就变成了无用功了。也许MVC4框架还在不断完善中,喜欢使用新技术应用于新项目的朋友需要谨慎一些。

  • 相关阅读:
    N皇后
    逆波兰中缀转后缀代码
    ImportError: No module named Image
    稳定排序 和 不稳定排序 ::::::::::: 内排序和外排序
    逆波兰中 中缀表达式转后缀表达式的方法
    第一章:Unix基础知识
    软件设计流程
    linux 下 tar.xz 文件的解压方法
    fedora 解决 Python.h:没有那个文件或目录 错误的方法
    Shell排序
  • 原文地址:https://www.cnblogs.com/guogangj/p/2718360.html
Copyright © 2011-2022 走看看