首页 > 代码库 > ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上

ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上

原文:ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上

ASP.NET MVC默认采用基于标准特性的Model验证机制,但是只有应用在Model类型及其属性上的ValidationAttribute才有效。如果我们能够将ValidationAttribute特性直接应用到参数上,我们不但可以实现简单类型(比如int、double等)数据的Model验证,还能够实现“一个Model类型,多种验证规则”,本篇文章将为你提供相关的解决方案(源代码从这里下载)。[本文已经同步到《How ASP.NET MVC Works?》中]

目录
一、ValidationAttribute本身是可以应用到参数上的
二、为什么需要基于参数的Model验证?
三、如何得到应用在参数上的ValidationAttribute?
四、自定义ModelValidatorProvider
五、自定义ModelBinder
六、实例演示

一、ValidationAttribute本身是可以应用到参数上的

如果你够细心应该会发现我们常用的验证特性都可以直接应用到方法的参数上。以如下所示的RangeAttribute的定义为例,应用在该类型上的AttributeUsageAttribute的定义表明可以标注该特性的目标元素包括参数、字段和属性。

   1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property,AllowMultiple=false)]
   2: public class RangeAttribute : ValidationAttribute
   3: {
   4:    //省略成员
   5: }

但是对于ASP.NET MVC的Model验证来说,应用在Action方法参数上的验证特性起不到任何作用,原因很简单:用于进行Model验证的ModelValidator对象是通过基于参数类型的Model元数据来创建的,根本不会去解析应用在参数本身上的验证特性

二、为什么需要基于参数的Model验证?

但是在我看到,直接针对Action参数的Model验证具有很高的实用意义:

  • 有些情况下我们不能对作为Model的数据类型进行修改(比如像int、double和字符串这样的原生类型);
  • 相同的Model类型在不同的Action方法调用中需要采用不同的验证规则。

如果我们可以直接将验证特性应用到参数上面,这两个问题在一定程度上都可以得到解决。

三、如何得到应用在参数上的ValidationAttribute?

到目前为止,我们对ASP.NET MVC的可扩展的Model验证系统已经有了一个全面的了解,现在我们通过对它进行相应的扩展使直接应用到参数上的验证特性能够生效。我们需要自定义一个ModelValidatorProvider将提供基于应用到参数上的验证特性的ModelValidator,但在这之前需要解决的另一个问题是如何将应用于参数的特性提供给我们自定义的ModelValidatorProvider。在这里我们将当前ControllerContext作为这些特性的载体。

Action方法的执行通过ActionInvoker来实现,默认的ControllerActionInvoker和AsyncControllerActionInvoker都定义了一个受保护的虚方法GetParameterValue根据用于描述参数的ParameterDescriptor对象和当前的Controller上下文来绑定对应的参数值。那么我们就可以通过继承ControllerActionInvoker/AsyncControllerActionInvoker以重写该方法的方式将ParameterDescriptor保存当前的Controller上下文中。

为此我们定义了一个具有如下定义的两个自定义的ActionInvoker。ParameterValidationActionInvoker和ParameterValidationAsyncActionInvoker分别继承自ControllerActionInvoker和AsyncControllerActionInvoker。在重写的GetParameterValue方法中,我们在调用基类的同名方法之前将作为参数的ParameterDescriptor对象保存到当前Controller上下文中,具体来说是放到了表示当前路由数据的RouteDataDictionary对象的DataTokens集合中。在方法调用之后我们将它从Controller上下文中移除。

   1: public class ParameterValidationActionInvoker : ControllerActionInvoker
   2: {
   3:     protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
   4:     {
   5:         try
   6:         {
   7:             controllerContext.RouteData.DataTokens.Add("ParameterDescriptor",parameterDescriptor);
   8:             return base.GetParameterValue(controllerContext, parameterDescriptor);
   9:         }
  10:         finally
  11:         {
  12:             controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
  13:         }
  14:     }
  15: }
  16:  
  17: public class ParameterValidationAsyncActionInvoker : AsyncControllerActionInvoker
  18: {
  19:     protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
  20:     {
  21:         try
  22:         {
  23:             controllerContext.RouteData.DataTokens.Add("ParameterDescriptor", parameterDescriptor);
  24:             return base.GetParameterValue(controllerContext, parameterDescriptor);
  25:         }
  26:         finally
  27:         {
  28:             controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
  29:         }
  30:     }
  31: }

四、自定义ModelValidatorProvider

ParameterValidationActionInvoker/ParameterValidationAsyncActionInvoker存放到当前Controller上下文中的ParameterDescriptor被我们自定义的ModelValidatorProvider提取出来用于创建相应的ModelValidator。如下面的代码片断所示,我们自定义的ParameterValidationModelValidatorProvider直接继承自DataAnnotationsModelValidatorProvider,在重写的GetValidators方法中我们将ParameterDescriptor从Controller上下文中提取出来,然后得到应用在参数上的所有的特性并与当前的特性列表进行合并,最后将合并的特性列表作为参数调用积累的GetValidators方法。

   1: public class ParameterValidationModelValidatorProvider : DataAnnotationsModelValidatorProvider
   2: {
   3:     protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
   4:     {    
   5:         object descriptor;
   6:         if (metadata.ContainerType == null && context.RouteData.DataTokens.TryGetValue("ParameterDescriptor", out descriptor))
   7:         {
   8:             ParameterDescriptor parameterDescriptor = (ParameterDescriptor)descriptor;
   9:             DisplayAttribute displayAttribute = parameterDescriptor.GetCustomAttributes(true).OfType<DisplayAttribute>().FirstOrDefault()
  10:                 ?? new DisplayAttribute { Name = parameterDescriptor.ParameterName };
  11:             metadata.DisplayName = displayAttribute.Name;
  12:             var addedAttributes = parameterDescriptor.GetCustomAttributes(true).OfType<Attribute>();
  13:             return base.GetValidators(metadata, context, attributes.Union(addedAttributes));
  14:         }
  15:         else
  16:         {
  17:             return base.GetValidators(metadata, context, attributes);
  18:         }
  19:     }
  20: }

值得一提的是,应用在参数上的特性是针对最外层的容器类型,而不是针对容器类型的属性的。比如所以我们在类型为Contact的参数上应用一个验证特性,该特性应该与应用在Contact类型上的特性具有相同的效果,但是与Address属性无关。所以ParameterDescriptor的提取以及特性的合并仅仅在当前Model元数据的ContainerType为Null的情况下才会进行。除此之外,我们还利用应用到参数的DisplayAttribute特性对Model元数据的DisplayName属性进行了相应的设置。

五、自定义ModelBinder

在默认的情况下,只有在针对复杂类型的Model绑定过程中才会进行Model验证。虽然我们通过ParameterValidationModelValidatorProvider能够根据应用在Action方法参数上的验证特性生成相应的ModelValidator,但是如果验证特性是应用在一个简单类型的参数上,生成出来的ModelValidator也是不会被使用的。为了使Model验证发生在针对简单类型的Model绑定过程中,我们不得不创建一个自定义的ModelBinder。为此我们定义了一个具有如下定义的ParameterValidationModelBinder,它直接继承自DefaultModelBinder,而针对简单类型的Model验证定义在重写的BindModel方法中。

   1: public class ParameterValidationModelBinder : DefaultModelBinder
   2: {
   3:     public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   4:     {
   5:         object model = bindingContext.ModelMetadata.Model = base.BindModel(controllerContext, bindingContext);
   6:         ModelMetadata metadata = http://www.mamicode.com/bindingContext.ModelMetadata;
   7:         if (metadata.IsComplexType || null == model)
   8:         {
   9:             return model;
  10:         }
  11:  
  12:         Dictionary<string, bool> dictionary = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
  13:         foreach (ModelValidationResult result in ModelValidator.GetModelValidator(metadata, controllerContext).Validate(null))
  14:         {
  15:             string key = bindingContext.ModelName;
  16:             if (!dictionary.ContainsKey(key))
  17:             {
  18:                 dictionary[key] = bindingContext.ModelState.IsValidField(key);
  19:             }
  20:             if (dictionary[key])
  21:             {
  22:                 bindingContext.ModelState.AddModelError(key, result.Message);
  23:             }
  24:         }
  25:         return model;
  26:     }
  27: }

到此为止,为了能够将验证特性应用于Action方法的参数,我们创建了自定义的ActionInvoker、ModelValidatorProvider和ModelBinder。为了验证它们是否能够最终实现我们期望的验证效果,我们将它们应用到一个简单的ASP.NET MVC应用中。

六、实例演示

在通过Visual Studio的ASP.NET MVC项目模板创建的空的Web应用中,我们创建了一个具有如下定义的HomeController。我们重写了CreateActionInvoker方法,如果调用基类同名方法返回一个ControllerActionInvoker对象,那么我们返回一个ParameterValidationActionInvoker对象,否则返回一个ParameterValidationAsyncActionInvoker对象,这是与默认的同步/异步Action执行方式保持一致。

   1: public class HomeController : Controller
   2: {
   3:     protected override IActionInvoker CreateActionInvoker()
   4:     {
   5:         IActionInvoker actionInvoker = base.CreateActionInvoker();
   6:         if (actionInvoker is ControllerActionInvoker)
   7:         {
   8:             return new ParameterValidationActionInvoker();
   9:         }
  10:         else
  11:         {
  12:             return new ParameterValidationAsyncActionInvoker();
  13:         }
  14:     }
  15:  
  16:     public ActionResult Add(
  17:         [Range(10, 20, ErrorMessage="{0}必须在{1}和{2}之间!")]
  18:         [ModelBinder(typeof(ParameterValidationModelBinder))]
  19:         [Display(Name = "第一个操作数")]
  20:         double x,
  21:  
  22:         [Range(20, 30,ErrorMessage="{0}必须在{1}和{2}之间!")]
  23:         [ModelBinder(typeof(ParameterValidationModelBinder))]
  24:         [Display(Name = "第二个操作数")]
  25:         double y)
  26:     {
  27:         return View(x + y);
  28:     }
  29: }

Action方法Add表示一个用于进行加法运算的操作,表示操作数的两个参数x和y分别应用了一个RangeAttribute特性将允许值得范围设置为10到20和20到30,并设置了相应的错误消息。此外,两个参数还通过应用ModelBinderAttribute特性使我们自定义的ParameterValidationModelBinder参与到这两个参数Model绑定中。DisplayAttribute特性也应用到这两个参数上对显示名称进行了相应的设置。作于执行加法运算后的结果通过默认的View呈现出来。下面的代码片断表示Action方法Add对应的View的定义,这是一个Model类型为double的强类型View。我们通过一个ValidationSummary来呈现验证的错误消息,只有在验证成功的情况下我们才真正显示运算的结果。

   1: @model double
   2: @Html.ValidationSummary()
   3: @{
   4:    if(ViewData.ModelState.IsValid)
   5:    {
   6:         @:运算结果:@Model
   7:    }
   8: }

然后我们在Global.asax中对自定义的ParameterValidationModelValidatorProvider进行注册。如下面的代码片断所示,在注册ParameterValidationModelValidatorProvider之前需要将现有的DataAnnotationsModelValidatorProvider移除。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     //其他成员
   4:     protected void Application_Start()
   5:     {
   6:         //其他操作
   7:         DataAnnotationsModelValidatorProvider validatorProvider = ModelValidatorProviders.Providers
   8:            .OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();
   9:         if (null != validatorProvider)
  10:         {
  11:             ModelValidatorProviders.Providers.Remove(validatorProvider);
  12:         }
  13:         ModelValidatorProviders.Providers.Add(new ParameterValidationModelValidatorProvider());
  14:     }
  15: }

我们运行该程序通过在浏览器中输入相应的地址来访问定义在HomeController中的Add操作,并以查询字符串的形式指定该Action方法的两个操作数(x=9,y=31)。由于提供的参数不服务应用在参数上的 RangeAttribute所定义的验证规则,如下图所示的错误消息会自动呈现出来。

image

ASP.NET MVC基于标注特性的Model验证:ValidationAttribute
ASP.NET MVC基于标注特性的Model验证:DataAnnotationsModelValidator
ASP.NET MVC基于标注特性的Model验证:DataAnnotationsModelValidatorProvider
ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上
ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则