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的描述:
根据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框架还在不断完善中,喜欢使用新技术应用于新项目的朋友需要谨慎一些。