开发者社区> 行者武松> 正文

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

简介:
+关注继续查看

ASP.NET MVC提供一种基于元数据的验证方式是我们可以将相应的验证特性应用到作为Model实体的类型或者属性/字段上,但是这依然具有很多的不足。在这篇文章中,我结合EntLib的VAB(Validation Application Block)的一些思想通过扩展为ASP.NET MVC提供一种更为完善的验证机制。[源代码从这里下载]

目录:
一、扩展旨在解决怎样的验证问题
二、一个简单的消息维护组件
三、多语言的支持
四、基于某个验证规则的验证
五、验证规则的一致性

一、扩展旨在解决怎样的验证问题

这个基于验证的扩展可以实现如下几个ASP.NET MVC无法实现验证问题:

  • 消息提供机制的分离:目前我们可以通过“硬编码”和“资源文件”两种验证错误消息的提供机制,但是如果能够提供一种独立的机制来提供验证的错误消息无疑是一种更好的选择。原因很简单,验证消息是呈现给最终的用户的,应该是可以单独进行维护的,当我们发现某个验证消息不够友好,应该以一种对现有应用毫无影响的方式进行修改。此外,消息的定义最好是基于“模板”,模板中定义相应的占位符,这样可以省去很多冗余消息的定义。比如对于某个区间的验证消息就可以定义成“{0}必须在{1}与{2}之间”;
  • 多语言的支持:和ASP.NET MVC基于资源文件(所有的ValidationAttribute可以通过指定属性Name和ResourceType使我们可以在资源文件中定义相应的消息)不同,消息模板对多语言的支持可以通过独立的消息维护组件/框架来解决,但是我们需要解决用于替换占位符的参数的多语言支持;
  • 多验证规则的支持:对于同一个实体对象,在不同的场景中具有不同的验证规则。比如说我们做一个招聘网站,针对不同工作岗位对应聘者的性别、年龄、学历、身高和体重等属性的要求都是不一样的,所以我们应该针对基于工作岗位的验证场景定义不同的验证规则,并针对某个具体的验证规则对实体对象实施验证。

二、一个简单的消息维护组件

为了演示消息提供机制的分离,我们定义了一个简单的消息维护组件MessageManager。如下面的代码所示,抽象类MessageManager具有唯一的FormatMessage方法用于获取一个经过格式化好的最终消息文本,参数category、id和args分别代表对应消息条目的类型、ID和作为替换占位符的参数。

   1: public abstract  class MessageManager
   2: {
   3:     public abstract string FormatMessage(string category, string id, params object[] args);
   4: }

我们定义了如下一个默认的DefaultMessageManager,它维护了一组代表消息条目的MessageEntry列表,而MessageEntry是支持多语言的。在重写的FormatMessage方法中,直接通过类型和ID在列表中找到相应的MessageEntry,并传输占位符参数根据当前线程的CurrentUICulture对消息文本进行格式。从如下的代码可以看出,我们仅仅定义了一个表示“必需字段”的消息,在en-US和zh-CN这两种语言文化下的文本分别是“{0} is mandatory!”和“请输入{0}!”。该MessageEntry得类型和ID分别是“Validation”和“MandatoryField”。

   1: public class DefaultMessageManager : MessageManager
   2: {
   3:     public DefaultMessageManager()
   4:     {
   5:         var messages = new List<MessageEntry>();
   6:         var messageEntry = new MessageEntry("Validation", "MandatoryField");
   7:         messageEntry.AddMessageText("{0} is mandatory!", new CultureInfo("en-US"));
   8:         messageEntry.AddMessageText("请输入{0}!", new CultureInfo("zh-CN"));
   9:         messages.Add(messageEntry);
  10:         this.Messages = messages;
  11:     }
  12:  
  13:     public IEnumerable<MessageEntry> Messages { get; private set; } 
  14:     public override string FormatMessage(string category, string id, params object[] args)
  15:     {
  16:         MessageEntry messageEntry = (from message in this.Messages
  17:                                      where message.Category == category && message.Id == id
  18:                                      select message).FirstOrDefault();
  19:         if (null == messageEntry)
  20:         {
  21:             throw new Exception("...");
  22:         }
  23:  
  24:         return messageEntry.Format(args);
  25:     }
  26: }

我们并没有列出MessageEntry的定义,有兴趣的朋友可以下载本例的源代码。最终我们定义了如下静态工厂MessageManagerFactory来创建相应的MessageManager,简单起见,我们直接创建上述的DefaultMessageManager。

   1: public static class MessageManagerFactory
   2: {
   3:     public static MessageManager GetMessageManager()
   4:     {
   5:         return new DefaultMessageManager();
   6:     }
   7: }

三、多语言的支持

在本篇文章中我们不谈具体实现,只谈具体的使用方法。我们以登录场景为例,如下所示的LoginInfo类型表示包含代表用户名和密码的Model类型。应用在属性上的RequiredValidatorAttribute特性是我们自定义的ValidationAttribute,它实现了RequiredAttribute一样的验证功能。以应用在UserName属性上的RequiredValidatorAttribute为例([RequiredValidator("Validation", "MandatoryField", "用户名", Name = "RequiredValidator", Culture = "zh-CN")]),构造函数参数分别代表通过MessageManager维护的对应消息条目的类型(Validation)、ID(MandatoryField)以及占位符参数(用户名)。Culture属性则代表对应的语言文化,如果没有对该属性进行显式指定,则代表“语言文化中性”的验证器。

   1: public class LoginInfo
   2: {
   3:     [Display(ResourceType = typeof(Resources), Name = "UserName")]
   4:     [RequiredValidator("Validation", "MandatoryField", "User Name")]
   5:     [RequiredValidator("Validation", "MandatoryField", "用户名", Culture = "zh-CN")]
   6:     public string UserName { get; set; }
   7:  
   8:     [RequiredValidator("Validation", "MandatoryField", "Password")]
   9:     [RequiredValidator("Validation", "MandatoryField", "密码", Culture = "zh-CN")]
  10:     [DataType(DataType.Password)]
  11:     [Display(ResourceType = typeof(Resources), Name = "Password")]
  12:     public string Password { get; set; }
  13: }

在进行验证器的选择的过程中,总是会根据当前线程的CurrentUICulture选择相匹配的验证器。如果找不到完全匹配的验证器,则会选择语言文化中性验证器(这样的验证器只允许有一个)。对于本例来说,如果当前的语言文化为zh-CN,那么只有应用在UserName和Password属性上Culture属性为zh-CN的RequiredValidatorAttribute有效,而在其他的语言文化环境中则会选择没有对Culture属性进行显式设置的RequiredValidatorAttribute。我们来看看用于进行用户登录的AccountController的定义:

   1: public class AccountController : BaseController
   2:     {
   3:         public ActionResult SignIn()
   4:         {
   5:             return View(new LoginInfo());
   6:         }
   7:         [HttpPost]
   8:         public ActionResult SignIn(LoginInfo logInfo)
   9:         {
  10:             if (ModelState.IsValid)
  11:             {
  12:                 return this.View();
  13:             }
  14:             else
  15:             {
  16:                 return this.View();
  17:             }
  18:         }
  19:     }

下面是SignIn操作默认的View的所有内容:

   1: @using Artech.Mvc.Validation.Properties
   2: @using Artech.Mvc.Validation.Models
   3: @model LoginInfo
   4:  
   5: @{
   6:     ViewBag.Title = "SignIn";
   7: }
   8: @Html.ValidationSummary()
   9: @using(Html.BeginForm())
  10: {
  11:     @Html.EditorForModel()
  12:     <input type="submit" value="@Resources.SignIn"/>
  13: }

在我们的例子中语言的设置是通过URL来体现的,为了我们在Global.asax中进行了如下的路由映射,即controller之前的部分代表语言文化代码,默认为zh-CN。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     public static void RegisterGlobalFilters(GlobalFilterCollection filters)
   4:     {
   5:         filters.Add(new HandleErrorAttribute());
   6:     }
   7:  
   8:     public static void RegisterRoutes(RouteCollection routes)
   9:     {
  10:          //...
  11:          routes.MapRoute(
  12:             "Default", // Route name
  13:             "{culture}/{controller}/{action}/{id}", // URL with parameters
  14:             new {culture="zh-CN", controller = "Account", action = "SignIn", id = UrlParameter.Optional } // Parameter defaults
  15:         );
  16:  
  17:     }
  18:  
  19:     protected void Application_Start()
  20:     {   
  21:         //...
  22:         RegisterRoutes(RouteTable.Routes);
  23:     }
  24: }

运行我们的程序并在分别以en-US和zh-CN访问主页,在没有输入用户名和密码的情况下将会得到如下的验证消息。

image

四、基于某个验证规则的验证

现在我们来演示基于某个验证规则的验证方式。对于登录,我们都应该有这样的体会,在开发阶段为了测试的时候避免频繁地输入用户名和密码,我们会设置一个默认的密码。在这里我们可以通过定义验证规则来屏蔽对密码的验证。为此我们我们对应用在LoginInfo的Password属性上的RequiredValidatorAttribute特性稍加改动,对其RuleName属性进行了显式设置(RuleName = "Production"),意味着只有当前验证规则为“Production”(产品阶段)的时候,基于它们的验证才会生效。

   1: public class LoginInfo
   2: {
   3:     //...
   4:     [RequiredValidator("Validation", "MandatoryField", "Password",RuleName = "Production")]
   5:     [RequiredValidator("Validation", "MandatoryField", "密码", Culture = "zh-CN", RuleName = "Production")]
   6:     [DataType(DataType.Password)]
   7:     [Display(ResourceType = typeof(Resources), Name = "Password")]
   8:     public string Password { get; set; }
   9: }

而对当前采用怎样地验证规则,则可以在Controller或者Action方法上应用我们自定义的RuleNameAttribute来设定。如下面的代码片断所示,我们在AccountController上直接应用了RuleNameAttribute特性并将当前的验证规则设置为“Dev”(开发阶段)。

   1: [ValidationRule("Dev")]
   2: public class AccountController : BaseController
   3: {
   4:     //...
   5: }

那么在程序运行的时候就不会对密码进行任何验证,这可以通过如下的截图可以看出来:

image

如果我们通过应用在AccountController上的RuleNameAttribute将验证规则设置为“Production”

   1: [ValidationRule("Production")]
   2: public class AccountController : BaseController
   3: {
   4:     //...
   5: }

那么针对密码的验证就会生效了:

image

五、验证规则的一致性

值得一提的是:我们扩展的验证体系依然也为客户端认证提供支持,但是在进行基于验证规则的验证是确有一个小小的机关。同样以AccountController的两个SignIn操作为例,进行客户端验证的规则是基于第一个SignIn操作(HttpGet)生成的,服务端验证则是基于第二个SignIn操作(HttpPost)的验证规则进行的,如果我们将RuleNameAttribute应用到两个SignIn操作上,比如确保它们的规则名称一致方能保证客户端验证和服务端认证的一致性。

   1: public class AccountController : BaseController
   2: {
   3:     [ValidationRule("Production")]
   4:     public ActionResult SignIn()
   5:     {
   6:         //...
   7:     }
   8:     [HttpPost]
   9:     [ValidationRule("Production")]
  10:     public ActionResult SignIn(LoginInfo logInfo)
  11:     {
  12:          //...
  13:     }
  14: }

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
28958 0
通过手动创建统计信息优化sql查询性能案例
本质原因在于:SQL Server 统计信息只包含复合索引的第一个列的信息,而不包含复合索引数据组合的信息   来源于工作中的一个实际问题, 这里是组合列数据不均匀导致查询无法预估数据行数,从而导致无法选择合理的执行计划导致性能低下的情况 我这里把问题简单化,主要是为了说明问题 如下一张业务表,...
815 0
[MSDN]通过避免下列 10 个常见 ASP.NET 缺陷使网站平稳运行
原文:http://www.microsoft.com/china/msdn/library/webservices/asp.net/WebAppFollies.mspx?mfr=true ASP.NET 成功的其中一个原因在于它降低了 Web 开发人员的门槛。
1020 0
ASP.NET MVC 音乐商店 - 5. 通过支架创建编辑表单
转自 http://www.cnblogs.com/haogj/archive/2011/11/15/2249143.html 在上一章,我们已经从数据库获取数据,然后显示出来,这一章,我们将允许编辑数据。
759 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
20616 0
+关注
行者武松
杀人者,打虎武松也。
17112
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载