原文:通过扩展改善ASP.NET MVC的验证机制[实现篇]

在《使用篇》中我们谈到扩展的验证编程方式,并且演示了本解决方案的三大特性:消息提供机制的分离、多语言的支持和多验证规则的支持,我们现在来看看这样的验证解决方案最终是如何实现的。

目录:
一、为验证创建一个上下文:ValidatorContext
二、通过自定义ActionInvoker在进行操作执行之前初始化上下文
三、为Validator创建基类:ValidatorBaseAttribute
四、通过自定义ModelValidatorProvider在验证之前将不匹配Validator移除
五、RequiredValidatorAttribute的定义

一、为验证创建一个上下文:ValidatorContext

“基于某个规则的验证”是本解决方案一个最大的卖点。为了保持以验证规则名称为核心的上下文信息,我定义了如下一个ValidatorContext(我们本打算将其命名为ValidationContext,无奈这个类型已经存在)。ValidatorContext的属性RuleName和Culture表示当前的验证规则和语言文化(默认值为当前线程的CurrentUICulture),而字典类型的属性Properties用户存放一些额外信息。当前ValidationContext的获取与设置通过静态Current完成。

   1: public class ValidatorContext

   2: {

   3:     [ThreadStatic]

   4:     private static ValidatorContext current;

   5:  

   6:     public string RuleName { get; private set; }

   7:     public CultureInfo Culture { get; private set; }

   8:     public IDictionary<string, object> Properties { get; private set; }

   9:  

  10:     public ValidatorContext(string ruleName, CultureInfo culture=null)

  11:     {

  12:         this.RuleName = ruleName;

  13:         this.Properties = new Dictionary<string, object>();

  14:         this.Culture = culture??CultureInfo.CurrentUICulture;

  15:     }

  16:  

  17:     public static ValidatorContext Current

  18:     {

  19:         get { return current; }

  20:         set { current = value; }

  21:     }

  22: }

我们为ValidatorContext定义了如下一个匹配的ValidatorContextScope对象用于设置ValidatorContext的作用范围。

   1: public class ValidatorContextScope : IDisposable

   2: {

   3:     private ValidatorContext current = ValidatorContext.Current;

   4:     public ValidatorContextScope(string ruleName, CultureInfo culture = null)

   5:     {

   6:         ValidatorContext.Current = new ValidatorContext(ruleName, culture);

   7:     }

   8:     public void Dispose()

   9:     {

  10:         if (null == current)

  11:         {

  12:             foreach (object property in ValidatorContext.Current.Properties.Values)

  13:             {

  14:                 IDisposable disposable = property as IDisposable;

  15:                 if (null != disposable)

  16:                 {

  17:                     disposable.Dispose();

  18:                 }

  19:             }

  20:         }

  21:         ValidatorContext.Current = current;

  22:     }

  23: }

二、通过自定义ActionInvoker在进行操作执行之前初始化上下文

通过《使用篇》中我们知道当前的验证规则名称是通过ValidationRuleAttribute来设置的,该特性不仅仅可以应用在Action方法上,也可以应用在Controller类型上。当然Action方法上的ValidationRuleAttribute具有更高的优先级。如下面的代码片断所示,ValidationRuleAttribute就是一个包含Name属性的普通Attribute而已。

   1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]

   2: public class ValidationRuleAttribute:Attribute

   3: {

   4:     public string Name { get; private set; }

   5:     public ValidationRuleAttribute(string name)

   6:     {

   7:         this.Name = name;

   8:     }

   9: }

很显然,以当前验证规则验证规则为核心的ValidatorContext需要在Action操作之前设置(严格地说应该在进行Model绑定之前),而在Action操作完成后清除。很自然地,我们可以通过自定义ActionInvoker来完成,为此我定义了如下一个直接继承自ControllerActionInvoker的ExtendedControllerActionInvoker类。

   1: public class ExtendedControllerActionInvoker : ControllerActionInvoker

   2: {

   3:     public ExtendedControllerActionInvoker()

   4:     { 

   5:         this.CurrentCultureAccessor= (context=>

   6:             {

   7:                 string culture = context.RouteData.GetRequiredString("culture");

   8:                 if(string.IsNullOrEmpty(culture))

   9:                 {

  10:                     return null;

  11:                 }

  12:                 else

  13:                 {

  14:                     return new CultureInfo(culture);

  15:                 }

  16:             });

  17:     }

  18:     public virtual Func<ControllerContext, CultureInfo> CurrentCultureAccessor { get; set; }

  19:     public override bool InvokeAction(ControllerContext controllerContext, string actionName)

  20:     {

  21:         CultureInfo originalCulture = CultureInfo.CurrentCulture;

  22:         CultureInfo originalUICulture = CultureInfo.CurrentUICulture;

  23:         try

  24:         {

  25:             CultureInfo culture = this.CurrentCultureAccessor(controllerContext);

  26:             if (null != culture)

  27:             {

  28:                 Thread.CurrentThread.CurrentCulture = culture;

  29:                 Thread.CurrentThread.CurrentUICulture = culture;

  30:             }

  31:             var controllerDescriptor = this.GetControllerDescriptor(controllerContext);

  32:             var actionDescriptor = this.FindAction(controllerContext, controllerDescriptor, actionName);

  33:             ValidationRuleAttribute attribute = actionDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() as ValidationRuleAttribute;

  34:             if (null == attribute)

  35:             {

  36:                 attribute = controllerDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() as ValidationRuleAttribute;

  37:             }

  38:             string ruleName = (null == attribute) ? string.Empty : attribute.Name;

  39:             using (ValidatorContextScope contextScope = new ValidatorContextScope(ruleName))

  40:             {

  41:                 return base.InvokeAction(controllerContext, actionName);

  42:             }

  43:         }

  44:         catch

  45:         {

  46:             throw;

  47:         }

  48:         finally

  49:         {

  50:             Thread.CurrentThread.CurrentCulture = originalCulture;

  51:             Thread.CurrentThread.CurrentUICulture = originalUICulture;

  52:         }

  53:     }

  54: }

如上面的代码片断所示,在重写的InvokeAction方法中我们通过ControllerDescriptor/ActionDescriptor得到应用在Controller类型/Action方法上的ValidationRuleAttribute特性,并或者到设置的验证规则名称。然后我们创建ValidatorContextScope对象,而针对基类InvokeAction方法的执行就在该ValidatorContextScope中执行的。初次之外,我们还对当前线程的Culture进行了相应地设置,默认的Culture 信息来源于当前RouteData。

为了更方便地使用ExtendedControllerActionInvoker,我们定义了一个抽象的Controller基类:BaseController。BaseController是Controller的子类,在构造函数中我们将ActionInvoker属性设置成我们自定义的ExtendedControllerActionInvoker对象。

   1: public abstract class BaseController: Controller

   2: {

   3:     public BaseController()

   4:     {

   5:         this.ActionInvoker = new ExtendedControllerActionInvoker();

   6:     }

   7: }

三、为Validator创建基类:ValidatorBaseAttribute

接下来我们才来看看真正用于验证的验证特性如何定义。我们的验证特性都直接或者间接地继承自具有如下定义的ValidatorBaseAttribute,而它使ValidationAttribute的子类。如下面的代码片断所示,ValidatorBaseAttribute还实现了IClientValidatable接口,以提供对客户端验证的支持。属性RuleName、MessageCategory、MessageId和Culture分别代表验证规则名称、错误消息的类别和ID号(通过这两个属性通过MessageManager这个独立的组件获取完整的错误消息)和基于的语言文化。

   1: [AttributeUsage(AttributeTargets.Class|AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]

   2: public abstract class ValidatorBaseAttribute : ValidationAttribute, IClientValidatable

   3: {

   4:     

   5:     public string RuleName { get; set; }

   6:     public string MessageCategory { get; private set; }

   7:     public string MessageId { get; private set; }

   8:     public string Culture { get; set; }

   9:  

  10:     public ValidatorBaseAttribute(MessageManager messageManager, string messageCategory, string messageId, params object[] args)

  11:         : base(() => messageManager.FormatMessage(messageCategory, messageId, args))

  12:     {

  13:         this.MessageCategory = messageCategory;

  14:         this.MessageId = messageId;

  15:     }

  16:  

  17:     public ValidatorBaseAttribute(string messageCategory, string messageId, params object[] args)

  18:         : this(MessageManagerFactory.GetMessageManager(), messageCategory, messageId, args)

  19:     { }

  20:  

  21:     public virtual bool Match(ValidatorContext context, IEnumerable<ValidatorBaseAttribute> validators)

  22:     {

  23:         if (!string.IsNullOrEmpty(this.RuleName))

  24:         {

  25:             if (this.RuleName != context.RuleName)

  26:             {

  27:                 return false;

  28:             }

  29:         }

  30:  

  31:         if (!string.IsNullOrEmpty(this.Culture))

  32:         {                

  33:             if (string.Compare(this.Culture, context.Culture.Name, true) != 0)

  34:             {

  35:                 return false;

  36:             }

  37:         }

  38:  

  39:         if (string.IsNullOrEmpty(this.Culture))

  40:         {

  41:             if (validators.Any(validator => validator.GetType() == this.GetType() && string.Compare(validator.Culture, context.Culture.Name, true) == 0))

  42:             {

  43:                 return false;

  44:             }

  45:         }

  46:         return true;

  47:     }

  48:     public abstract IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context);

  49:     private object typeId;

  50:     public override object TypeId

  51:     {

  52:         get { return (null == typeId) ? (typeId = new object()) : typeId; }

  53:     }

  54: }

由于我们需要将多个相同类型的Validator特性应用到某个类型或者字段/属性上,我们需要通过AttributeUsageAttribute将AllowMultiple属性设置为True,此外需要重写TypeId属性。至于为什么需需要这么做,可以参考我的上一篇文章《在ASP.NET MVC中如何应用多个相同类型的ValidationAttribute?》。对于应用在同一个目标元素的多个相同类型的Validator特性,只有与当前ValidatorContext相匹配的才能执行,我们通过Match方法来进行匹配性的判断,具体的逻辑是这样的:

  • 在显式设置了RuleName属性情况下,如果不等于当前验证规则,直接返回False;
  • 在显式设置了Culture属性情况下,如果与当前语言文化不一致,直接返回False;
  • 在没有设置Culture属性(语言文化中性)情况下,如果存在另一个同类型的Validator与当前的语言文化一致,也返回False;
  • 其余情况返回True

四、通过自定义ModelValidatorProvider在验证之前将不匹配Validator移除

应用在Model类型或其属性/字段上的ValidationAttribute最终通过对应的ModelValidatorProvider(DataAnnotationsModelValidatorProvider)用于创建ModelValidator(DataAnnotationsModelValidator)。我们必须在ModelValidator创建之前将不匹配的Validator特性移除,才能确保只有与当前ValidatorContext相匹配的Validator特性参与验证。为此我们通过继承DataAnnotationsModelValidator自定义了如下一个ExtendedDataAnnotationsModelValidator。

   1: public class ExtendedDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider

   2: {

   3:     protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)

   4:     {

   5:         var validators = attributes.OfType<ValidatorBaseAttribute>();

   6:         var allAttributes = attributes.Except(validators).ToList();

   7:         foreach (ValidatorBaseAttribute validator in validators)

   8:         {

   9:             if (validator.Match(ValidatorContext.Current, validators))

  10:             {

  11:                 allAttributes.Add(validator);

  12:             }

  13:         }

  14:         return base.GetValidators(metadata, context, allAttributes);

  15:     }

  16: }

如上面的代码片断所示,在重写的GetClientValidationRules方法中,输入参数attributes表示所有的ValidationAttribute,在这里我们根据调用ValidatorBaseAttribute的Match方法将不匹配的Validator特性移除,然后根据余下的ValidationAttribute列表调用基类GetValidators方法创建ModelValidator列表。值得一提的是,关于System.Attribute的Equals/GetHashCode方法的问题就从这个方法中发现的(详情参见《为什么System.Attribute的GetHashCode方法需要如此设计?》)。自定义ExtendedDataAnnotationsModelValidator在Global.asax的Application_Start方法中通过如下的方式进行注册。

   1: protected void Application_Start()

   2: {

   3:      //...

   4:     var provider = ModelValidatorProviders.Providers.OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();

   5:     if (null != provider)

   6:     {

   7:         ModelValidatorProviders.Providers.Remove(provider);

   8:     }

   9:     ModelValidatorProviders.Providers.Add(new ExtendedDataAnnotationsModelValidatorProvider());

  10: }

五、RequiredValidatorAttribute的定义

最后我们来看看用于验证必需字段的RequiredValidatorAttribute如何定义。IsValid用于服务端验证,而GetClientValidationRules生成调用客户端验证规则。

   1: [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]

   2: public class RequiredValidatorAttribute : ValidatorBaseAttribute

   3: {

   4:     public RequiredValidatorAttribute(string messageCategory, string messageId, params object[] args)

   5:         : base(messageCategory, messageId, args)

   6:     { }   

   7:  

   8:     public override bool IsValid(object value)

   9:     {

  10:         if (value == null)

  11:         {

  12:             return false;

  13:         }

  14:         string str = value as string;

  15:         if (str != null)

  16:         {

  17:             return (str.Trim().Length != 0);

  18:         }

  19:         return true;

  20:     }

  21:  

  22:     public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)

  23:     {

  24:         return new ModelClientValidationRequiredRule[] { new ModelClientValidationRequiredRule(this.ErrorMessageString) };

  25:     }

  26: }

通过扩展改善ASP.NET MVC的验证机制[实现篇]的更多相关文章

  1. 通过扩展改善ASP.NET MVC的验证机制[使用篇]

    原文:通过扩展改善ASP.NET MVC的验证机制[使用篇] ASP.NET MVC提供一种基于元数据的验证方式是我们可以将相应的验证特性应用到作为Model实体的类型或者属性/字段上,但是这依然具有 ...

  2. 【MVC】ASP.NET MVC Forms验证机制

    http://www.cnblogs.com/bomo/p/3309766.html 随笔 - 121  文章 - 0  评论 - 92 [MVC]ASP.NET MVC Forms验证机制 ASP. ...

  3. ASP.NET MVC Forms验证机制

    ASP.NET MVC 3 使用Forms身份验证 身份验证流程 一.用户登录 1.验证表单:ModelState.IsValid 2.验证用户名和密码:通过查询数据库验证 3.如果用户名和密码正确, ...

  4. ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  5. ASP.NET MVC 2 验证

    来源:http://www.cnblogs.com/jhxk/articles/2612885.html  只为把自己觉的好的存起来 对用户输入的验证以及强制业务规则/逻辑是大多数web应用的核心需求 ...

  6. Asp.Net MVC 身份验证-Forms

    Asp.Net MVC 身份验证-Forms 在MVC中对于需要登录才可以访问的页面,只需要在对应的Controller或Action上添加特性[Authorize]就可以限制非登录用户访问该页面.那 ...

  7. ASP.NET MVC异步验证是如何工作的03,jquery.validate.unobtrusive.js是如何工作的

    在上一篇"ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建"中了解了ASP.NET异步验证是如何创建表单元素的,本篇体验jquery.validate.uno ...

  8. [转]ASP.NET MVC 5 - 验证编辑方法(Edit method)和编辑视图(Edit view)

    在本节中,您将验证电影控制器生成的编辑方法(Edit action methods)和视图.但是首先将修改点代码,使得发布日期属性(ReleaseDate)看上去更好.打开Models \ Movie ...

  9. ASP.NET MVC Model验证(五)

    ASP.NET MVC Model验证(五) 前言 上篇主要讲解ModelValidatorProvider 和ModelValidator两种类型的自定义实现, 然而在MVC框架中还给我们提供了其它 ...

随机推荐

  1. 【夯实基础】javakeywordsynchronized 详细说明

    尊重版权:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html Java语言的keyword.当它用来修饰一个方法或者一个代码 ...

  2. 程序猿常识--OJ系统和ACM测试考试大全

    OJ它是Online Judge缩写系统,来在线检測程序源码的正确性. 著名的OJ有RQNOJ.URAL等. 国内著名的题库有北京大学题库.浙江大学题库等. 国外的题库包含乌拉尔大学.瓦拉杜利德大学题 ...

  3. [Linux]history 显示命令的运行时间

    显示线时间历史命令 这里的环境是centos5.8 vim ~/.bashrc 或者 ~/.bash_profile 添加 export HISTTIMEFORMAT="%F %T &quo ...

  4. Linux学习笔记——如何使用共享库交叉编译

    0.前言     在较为复杂的项目中会利用到交叉编译得到的共享库(*.so文件).在这样的情况下便会产生下面疑问,比如:     [1]交叉编译时的共享库是否须要放置于目标板中,假设须要放置在哪个文件 ...

  5. 设备11g_rac配置对等

    linux平台安装oracle 11gssh同等配置简单 构造grid用户任关系 登陸rac1,rac2分别运行: $ su - grid $mkdir ~/.ssh $chmod 700 ~/.ss ...

  6. 终结者:具体解释Nginx(一)

            相信非常多人都听过Nginx.这个小巧的东西能够和Apache及IIS相媲美. 那么它有什么作用呢?一句话.它是一个减轻Web应用server(如Tomcat)压力和实现Web应用se ...

  7. jQuery的使用及关于框架造型(转)

    Introduction 正如jQuery所宣称的一样,Write Less, Do More.很多时候我们喜欢用它来解决问题.但增加一个库必然意味着更大的网络负担,意味着更高的页面初始载入时间.并且 ...

  8. 在Mac OS上配置Android开发环境

    1)安装配置NDK 1.1 下载NDK并解压缩 下载路径 https://developer.android.com/tools/sdk/ndk/index.html 在terminal运行: chm ...

  9. jQuery整理笔记文件夹

    jQuery整理笔记文件夹 jQuery整理笔记一----jQuery開始 jQuery整理笔记二----jQuery选择器整理 jQuery整理笔记三----jQuery过滤函数 jQuery整理笔 ...

  10. log4j+logback+slf4j+commons-logging的关系与调试(转)

    背景     由于现在开源框架日益丰富,好多开源框架使用的日志组件不尽相同.存在着在一个项目中,不同的版本,不同的框架共存.导致日志输出异常混乱.虽然也不至于对系统造成致命伤害,但是明显可以看出,架构 ...