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

简介: 原文:ASP.NET MVC基于标注特性的Model验证:将ValidationAttribute应用到参数上ASP.NET MVC默认采用基于标准特性的Model验证机制,但是只有应用在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.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 = 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:         
  18:         
  19:         
  20:         double x,
  21:  
  22:         
  23:         
  24:         
  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,多种验证规则

目录
相关文章
|
存储 Shell Linux
快速上手基于 BaGet 的脚本自动化构建 .net 应用打包
本文介绍了如何使用脚本自动化构建 `.net` 应用的 `nuget` 包并推送到指定服务仓库。首先概述了 `BaGet`——一个开源、轻量级且高性能的 `NuGet` 服务器,支持多种存储后端及配置选项。接着详细描述了 `BaGet` 的安装、配置及使用方法,并提供了 `PowerShell` 和 `Bash` 脚本实例,用于自动化推送 `.nupkg` 文件。最后总结了 `BaGet` 的优势及其在实际部署中的便捷性。
977 10
|
开发框架 .NET API
.NET 10首个预览版发布:重大改进与新特性概览!
.NET 10首个预览版发布:重大改进与新特性概览!
540 3
.NET 10首个预览版发布:重大改进与新特性概览!
|
C# Android开发 iOS开发
2025年全面的.NET跨平台应用框架推荐
2025年全面的.NET跨平台应用框架推荐
811 23
|
前端开发 IDE Java
Spring MVC 中因导入错误的 Model 类报错问题解析
在 Spring MVC 或 Spring Boot 开发中,若导入错误的 `Model` 类(如 `ch.qos.logback.core.model.Model`),会导致无法解析 `addAttribute` 方法的错误。正确类应为 `org.springframework.ui.Model`。此问题通常因 IDE 自动导入错误类引起。解决方法包括:删除错误导入、添加正确包路径、验证依赖及清理缓存。确保代码中正确使用 Spring 提供的 `Model` 接口以实现前后端数据传递。
509 0
|
人工智能 机器人
D1net阅闻 | 谷歌DeepMind研究发现LLM新特性
D1net阅闻 | 谷歌DeepMind研究发现LLM新特性
|
自然语言处理 物联网 图形学
.NET 技术凭借其独特的优势和特性,为开发者们提供了一种高效、可靠且富有创造力的开发体验
本文深入探讨了.NET技术的独特优势及其在多个领域的应用,包括企业级应用、Web应用、桌面应用、移动应用和游戏开发。通过强大的工具集、高效的代码管理、跨平台支持及稳定的性能,.NET为开发者提供了高效、可靠的开发体验,并面对技术更新和竞争压力,不断创新发展。
772 7
|
开发框架 .NET C#
.NET 技术凭借高效开发环境、强大框架支持及跨平台特性,在软件开发中占据重要地位
.NET 技术凭借高效开发环境、强大框架支持及跨平台特性,在软件开发中占据重要地位。从企业应用到电子商务,再到移动开发,.NET 均展现出卓越性能,助力开发者提升效率与项目质量,推动行业持续发展。
472 4
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
414 5
|
JSON 算法 安全
JWT Bearer 认证在 .NET Core 中的应用
【10月更文挑战第30天】JWT(JSON Web Token)是一种开放标准,用于在各方之间安全传输信息。它由头部、载荷和签名三部分组成,用于在用户和服务器之间传递声明。JWT Bearer 认证是一种基于令牌的认证方式,客户端在请求头中包含 JWT 令牌,服务器验证令牌的有效性后授权用户访问资源。在 .NET Core 中,通过安装 `Microsoft.AspNetCore.Authentication.JwtBearer` 包并配置认证服务,可以实现 JWT Bearer 认证。具体步骤包括安装 NuGet 包、配置认证服务、启用认证中间件、生成 JWT 令牌以及在控制器中使用认证信息
672 2
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
307 1