ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则
对于Model验证,理想的设计应该是场景驱动的,而不是Model(类型)驱动的,也就是对于同一个Model对象,在不同的使用场景中可能具有不同的验证规则。举个简单的例子,对于一个表示应聘者的数据对象来说,针对应聘的岗位不同,肯定对应聘者的年龄、性别、专业技能等方面有不同的要求。但是ASP.NET MVC的Model验证确是Model驱动的,因为验证规则以验证特性的形式应用到Model类型及其属性上。这样的验证方式实际上限制了Model类型在基于不同验证规则的使用场景中的重用。通过上一篇文章《将ValidationAttribute应用到参数上》的扩展我们将验证特性直接应用在参数上变成了可能,这从一定程度上解决了这个问题,但是只能解决部分问题,因为应用到参数的验证特性只能用于针对参数类型级别的验证,而不能用于针对参数类型属性级别的验证(源代码从这里下载)。[本文已经同步到《How ASP.NET MVC Works?》中]
目录
一、同一个Model在采用不同的验证规则
二、新的基类ValidatorAttribute
三、指定当前采用的验证规则:ValidationRuleAttribute
四、新的Controller基类:RuleBasedController
五、自定义ModelValidatorProvider:RuleBasedValidatorProvider
一、同一个Model在采用不同的验证规则
现在我们通过利用对ASP.NET MVC的扩展来实现一种基于不同验证规则的Model验证。为了让读者对这种认证方式有一个感官的认识,我们来看看这个扩展最终实现怎样的验证效果。在通过Visual Studio的ASP.NET MVC 项目模板创建的空Web应用中,我们定义了如下一个Person类型作为Model。
1: public class Person
2: {
3: [DisplayName("姓名")]
4: public string Name { get; set; }
5:
6: [DisplayName("性别")]
7: public string Gender { get; set; }
8:
9: [DisplayName("年龄")]
10: [RangeValidator(10, 20, RuleName = "Rule1", ErrorMessage = "{0}必须在{1}和{2}之间!")]
11: [RangeValidator(20, 30, RuleName = "Rule2", ErrorMessage = "{0}必须在{1}和{2}之间!")]
12: [RangeValidator(30, 40, RuleName = "Rule3", ErrorMessage = "{0}必须在{1}和{2}之间!")]
13: public int Age { get; set; }
14: }
在表示年龄的Age属性上应用了三个RangeValidatorAttribute(不是RangeAttribute),它们对应针对年龄的三种不同的验证规则,RuleName属性表示规则名称。三种验证规则(Rule1、Rule2和Rule3)分别要求年龄分别在10到20、20到30和30到40岁之间。
然后我们定义了具有如下定义HomeController,它具有三组Action方法(Index、Rule1和Rule2)。方法Rule1、Rule2和HomeController类上应用了一个ValidationRuleAttribute特性用于指定了当前采用的验证规则。用于指定验证规则的ValidationRuleAttribute特性可以同时应用于Controller类型和Action方法上,应用于后者的ValidationRuleAttribute特性具有更高的优先级。针对HomeController的定义,Action方法Index、Rule1和Rule2分别采用的验证规则为Rule3、Rule1和Rule2。
1: [ValidationRule("Rule3")]
2: public class HomeController : RuleBasedController
3: {
4: public ActionResult Index()
5: {
6: return View("person", new Person());
7: }
8: [HttpPost]
9: public ActionResult Index(Person person)
10: {
11: return View("person", person);
12: }
13:
14: [ValidationRule("Rule1")]
15: public ActionResult Rule1()
16: {
17: return View("person", new Person());
18: }
19: [HttpPost]
20: [ValidationRule("Rule1")]
21: public ActionResult Rule1(Person person)
22: {
23: return View("person", person);
24: }
25:
26: [ValidationRule("Rule2")]
27: public ActionResult Rule2()
28: {
29: return View("person", new Person());
30: }
31: [HttpPost]
32: [ValidationRule("Rule2")]
33: public ActionResult Rule2(Person person)
34: {
35: return View("person", person);
36: }
37: }
定义在HomeController中的6个方法均将创建的/接收的Person对象呈现到名为Person的View中,该View的定义如下所示。这是一个将Person类型作为Model的强类型View,在该View中我们将作为Model的Person对象以编辑模式呈现在一个表单中,并在表单中提供一个提交按钮。
1: @model Person
2: @using (Html.BeginForm())
3: {
4: @Html.EditorForModel()
5: <input type="submit" value="保存" />
6: }
现在运行我们的程序,并通过在浏览器中指定相应的地址分别访问定义在HomeController的三个Action(Index、Rule1和Rule2),一个用于编辑个人信息的表单会呈现出来。然后我们根据三个Action方法采用的验证规则输入不合法的年龄,然后点击“保存”按钮,我们会看到输入的年龄按照对应的规则被验证了,具体的验证效果如下图所示。
二、新的基类ValidatorAttribute
我们现在就来具体谈谈上面这个例子所展示的基于不同规则的Model验证是如何实现的。首先我们需要重建一套新的验证特性体系,只为我们能够指定具体的验证规则。为此我们定义了一个抽象的ValidatorAttribute类型,如下面的代码片断所示,ValidatorAttribute直接继承自ValidationAttribute,属性RuleName表示采用的验证规则名称。我们重写了TypeId属性,因为我们需要在相同的属性或者类型上应用多个同类的ValidatorAttribute。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Property,AllowMultiple = true)]
2: public abstract class ValidatorAttribute: ValidationAttribute
3: {
4: private object typeId;
5: public string RuleName { get; set; }
6: public override object TypeId
7: {
8: get{return typeId ?? (typeId = new object());}
9: }
10: }
上面演示实例采用的RangeValidatorAttribute定义如下,我们可以看到它仅仅是对RangeAttribute的封装。RangeValidatorAttribute具有与RangeAttribute一致的构造函数定义,并直接使用被封装的RangeAttribute实施验证。除了能够通过RuleName指定具体采用的验证规则之外,其他的使用方式与RangeAttribute完全一致。
1: [AttributeUsage( AttributeTargets.Property, AllowMultiple = true)]
2: public class RangeValidatorAttribute:ValidatorAttribute
3: {
4: private RangeAttribute rangeAttribute;
5: public RangeValidatorAttribute(int minimum, int maximum)
6: {
7: rangeAttribute = new RangeAttribute(minimum, maximum);
8: }
9: public RangeValidatorAttribute(double minimum, double maximum)
10: {
11: rangeAttribute = new RangeAttribute(minimum, maximum);
12: }
13: public RangeValidatorAttribute(Type type, string minimum, string maximum)
14: {
15: rangeAttribute = new RangeAttribute(type, minimum, maximum);
16: }
17: public override bool IsValid(object value)
18: {
19: return rangeAttribute.IsValid(value);
20: }
21:
22: public override string FormatErrorMessage(string name)
23: {
24: return string.Format(CultureInfo.CurrentCulture, base.ErrorMessageString, new object[] { name, rangeAttribute.Minimum, rangeAttribute.Maximum });
25: }
26: }
三、指定当前采用的验证规则:ValidationRuleAttribute
ValidatorAttribte的RuleName属性仅仅指定了验证特性采用的验证规则名称,当前应在采用的验证规则通过应用在Action方法或者Controller类型上的ValidationRuleAttribute特性还指定。如下所示的就是ValidationRuleAttribute的定义,它仅仅包含一个表示当前采用的验证规则名称的RuleName属性的特性而已。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method)]
2: public class ValidationRuleAttribute: Attribute
3: {
4: public string RuleName { get; private set; }
5: public ValidationRuleAttribute(string ruleName)
6: {
7: this.RuleName = ruleName;
8: }
9: }
四、新的Controller基类:RuleBasedController
对于这个用于实现针对不同验证规则的扩展来说,其核心是如何将通过ValidationRuleAttribute特性设置的验证规则应用到ModelValidator的提供机制中,使之筛选出与当前验证规则匹配的验证特性,在这里我们依然使用Controller上下文来保存这个这个验证规则名称。细心的读者应该留意到了上面演示实例中创建的HomeController不是继承自Controller,而是继承自RuleBasedController,这个自定义的Controller基类定义如下。
1: public class RuleBasedController: Controller
2: {
3: private static Dictionary<Type, ControllerDescriptor> controllerDescriptors = new Dictionary<Type, ControllerDescriptor>();
4: public ControllerDescriptor ControllerDescriptor
5: {
6: get
7: {
8: ControllerDescriptor controllerDescriptor;
9: if (controllerDescriptors.TryGetValue(this.GetType(), out controllerDescriptor))
10: {
11: return controllerDescriptor;
12: }
13: lock (controllerDescriptors)
14: {
15: if (!controllerDescriptors.TryGetValue(this.GetType(), out controllerDescriptor))
16: {
17: controllerDescriptor = new ReflectedControllerDescriptor(this.GetType());
18: controllerDescriptors.Add(this.GetType(), controllerDescriptor);
19: }
20: return controllerDescriptor;
21: }
22: }
23: }
24: protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
25: {
26: SetValidationRule();
27: return base.BeginExecuteCore(callback, state);
28: }
29: protected override void ExecuteCore()
30: {
31: SetValidationRule();
32: base.ExecuteCore();
33: }
34: private void SetValidationRule()
35: {
36: string actionName = this.ControllerContext.RouteData.GetRequiredString("action");
37: ActionDescriptor actionDescriptor = this.ControllerDescriptor.FindAction(this.ControllerContext, actionName);
38: if (null != actionDescriptor)
39: {
40: ValidationRuleAttribute validationRuleAttribute = actionDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() ??
41: this.ControllerDescriptor.GetCustomAttributes(true).OfType<ValidationRuleAttribute>().FirstOrDefault() ??
42: new ValidationRuleAttribute(string.Empty);
43: this.ControllerContext.RouteData.DataTokens.Add("ValidationRuleName", validationRuleAttribute.RuleName);
44: }
45: }
46: }
在继承自Controller的RuleBasedController中,ExecuteCore和BeginExecuteCore方法被重写,在调用基类的同名方法之前,方法SetValidationRule方法被调用将应用在当前Action方法或者Controller类型上的ValidationRuleAttribute特性指定的验证规则名称保存到当前Controller上下文中。由于对Action方法和Controller类上特性的解析需要使用到用于描述Controller的ControllerDescriptor对象,处于性能考虑,我们对该对象进行了全局缓存。
五、自定义ModelValidatorProvider:RuleBasedValidatorProvider
对于应用在同一个属性或者类型上的多个基于不同验证规则的ValidatorAttribute,对应的验证规则名称并没有应用到具体的验证逻辑中。以上面定义的RangeValidatorAttribute为例,具体的验证逻辑通过被封装的RangeAttribute来实现,如果我们不做任何的处理,所有的基于不同规则的RangeValidatorAttribute都还参与到最终的Model验证过程中。我们必须作的是在根据验证特性创建ModelValidator的时候只选择那些与当前验证规则一直的ValidatorAttribute,这样的操作实现在具有如下定义的RuleBasedValidatorProvider中。
1: public class RuleBasedValidatorProvider : DataAnnotationsModelValidatorProvider
2: {
3: protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
4: {
5: object validationRuleName = string.Empty;
6: context.RouteData.DataTokens.TryGetValue("ValidationRuleName", out validationRuleName);
7: string ruleName = validationRuleName.ToString();
8: attributes = this.FilterAttributes(attributes, ruleName);
9: return base.GetValidators(metadata, context, attributes);
10: }
11:
12: private IEnumerable<Attribute> FilterAttributes(IEnumerable<Attribute> attributes, string validationRule)
13: {
14: var validatorAttributes = attributes.OfType<ValidatorAttribute>();
15: var nonValidatorAttributes = attributes.Except(validatorAttributes);
16: List<ValidatorAttribute> validValidatorAttributes = new List<ValidatorAttribute>();
17:
18: if (string.IsNullOrEmpty(validationRule))
19: {
20: validValidatorAttributes.AddRange(validatorAttributes.Where(v => string.IsNullOrEmpty(v.RuleName)));
21: }
22: else
23: {
24: var groups = from validator in validatorAttributesgroup validator by validator.GetType();
25: foreach (var group in groups)
26: {
27: ValidatorAttribute validatorAttribute = group.Where(v => string.Compare(v.RuleName, validationRule, true) == 0).FirstOrDefault();
28: if (null != validatorAttribute)
29: {
30: validValidatorAttributes.Add(validatorAttribute);
31: }
32: else
33: {
34: validatorAttribute = group.Where(v => string.IsNullOrEmpty(v.RuleName)).FirstOrDefault();
35: if (null != validatorAttribute)
36: {
37: validValidatorAttributes.Add(validatorAttribute);
38: }
39: }
40: }
41: }
42: return nonValidatorAttributes.Union(validValidatorAttributes);
43: }
44: }
如上面的代码所示,RuleBasedValidatorProvider继承自DataAnnotationsModelValidatorProvider,基于当前验证规则(从当前的Controller上下文中提取)对ValidatorAttribute的筛选,以及ModelValidator的创建通过重写的GetValidators方法实现。具体的筛选机制是:如果当前的验证规则存在,则选择与之具有相同规则名称的第一个ValidatorAttribute,如果这样的ValidatorAttribute找不到,则选择第一个没有指定验证规则的ValidatorAttribute;如果当前的验证规则没有指定,那么也选择第一个没有指定验证规则的ValidatorAttribute。
在让我们的Controller继承自RuleBasedController之后,我们需要在Global.asax中通过如下的方式对自定义的RuleBasedValidatorProvider进行注册,然后我们的应用就能按照我们期望的方式根据你指定的验证规则实施Model验证了。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: //其他成员
4: protected void Application_Start()
5: {
6: //其他操作
7: DataAnnotationsModelValidatorProvider validator = ModelValidatorProviders.Providers.OfType<DataAnnotationsModelValidatorProvider>().FirstOrDefault();
8: if(null != validator)
9: {
10: ModelValidatorProviders.Providers.Remove(validator);
11: }
12: ModelValidatorProviders.Providers.Add(new RuleBasedValidatorProvider());
13: }
14: }