问题
事情来源于很早之前Team成员一个不规范的设计,在MVC3的项目上,由于所有的Model都需要有一些基本的名称或者操作,加之应用了DI,所以就想当然地定义了一个接口,里面包含了一些接口属性和方法,可突然有一天要求在这些属性上应用一些验证约束和授权,于是接口代码改成了这样:
public interface IModel
{
[Required]
string ModelName { get; set; }
[Permission(Configuration = "Debug")]
void OutputMessage();
}
实现类,几乎没有改变,只是在需要验证的属性上添加了Required类似的attribute:
public class SearchCriteria : IModel
{
public string ModelName { get; set; }
[Required]
public string Keyword { get; set; }
// 更多Model属性
public void OutputMessage()
{
// 这里是处理代码
}
}
可是后来应用的时候,发现接口里定义的验证约束根本不起作用,后来开始一路查找。
分析
看了MVC的源码,找到了2个相关的地方。
1.查询所有的Attributes(AssociatedValidatorProvider里定义了GetValidators抽象方法)
private IEnumerable<ModelValidator> GetValidatorsForType(ModelMetadata metadata, ControllerContext context) {
return GetValidators(metadata, context, GetTypeDescriptor(metadata.ModelType).GetAttributes().Cast<Attribute>());
}
private IEnumerable<ModelValidator> GetValidatorsForProperty(ModelMetadata metadata, ControllerContext context) {
ICustomTypeDescriptor typeDescriptor = GetTypeDescriptor(metadata.ContainerType);
PropertyDescriptor property = typeDescriptor.GetProperties().Find(metadata.PropertyName, true);
if (property == null) {
throw new ArgumentException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.Common_PropertyNotFound,
metadata.ContainerType.FullName, metadata.PropertyName),
"metadata");
}
return GetValidators(metadata, context, property.Attributes.OfType<Attribute>());
}
2.DataAnnotationsModelValidatorProvider类实现了GetValidators抽象方法,节选代码:
// Produce a validator for each validation attribute we find
foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>()) {
DataAnnotationsModelValidationFactory factory;
if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory)) {
factory = DefaultAttributeFactory;
}
results.Add(factory(metadata, context, attribute));
}
可是从里面根本没有发现有什么特殊处理的地方,那为何就不能把接口的Attributes取出来呢?
后来看到Brad Wilson的一篇文章,才发现原来CLR对于基类和接口的处理是不一样的,基类是继承的,所以所有基类里出现的方法,属性等在子类实现类里具有同样的效果,但是接口是不一样的,接口是用来实现的,要求子类必须实现,但接口有2种实现方式:隐式和显式。
隐式:也就是我们上面的代码使用的方式,用public的属性和方法去实现接口,但是重要的,实现类里的public的属性和方法和接口里声明的那些完全不是一样的东西,虽然他们有相同的签名,通过反射代码,我们可以看出两者之间完全不一样,因为这个不同,所以说接口属性和方法上的metadata Attribute不会作用在实现类上,也就会出现我们文章开头的问题。
这些规则其实是CLR定义好的,和MVC无关,查看MVC里关于model绑定的源码,我们可以知道,在进行model绑定的时候,我们是基于实现类类型的,而不是接口类型,因为action里的参数一般来说都是实现类类型(MVC隐式创建),而不是接口类型(MVC实现不了),所以在上述2段MVC源码里通过反射查找Attributes的时候只能查询到实现类类型的Attributes。
改进
知道了原因,我们就得改代码了,第一种最简单的方式就是在子类实现类上需要验证的属性上逐一添加相应的验证类型Attributes,后来觉得其实Model绑定也没必要非要得DI挂上钩,所以改成了抽象类,如下:
public abstract class ModelBase
{
[Required]
string ModelName { get; set; }
[Permission(Configuration = "Debug")]
void OutputMessage();
}
这样,就不用在子类实现类里逐一添加这些Attributes了。
当然,如果你非要使用接口,而又不想在子类里逐一添加Attributes,那恐怕你只有在MVC里使用自己的自定义ModelValidatorProvider了,在保留原来代码的基础上,加上一段特殊的逻辑,把该model所实现的接口逐一判断一下,看看里面有没有带ValidationAttribute,伪代码如下:
public List<ValidationAttribute> GetValidationAttributesFromInterface()
{
//以类型SearchCriteria为例
List<ValidationAttribute> attributes = new List<ValidationAttribute>();
typeof(SearchCriteria)
.GetInterfaces()
.ToList()
.ForEach(t =>
{
attributes.AddRange(
t.GetProperty("ModelName").
GetCustomAttributes(true).OfType
<ValidationAttribute>());
});
return attributes;
}
参考文档:http://bradwilson.typepad.com/blog/2011/08/interface-attributes-class-attributes.html
同步与结束语
本文已同步至目录索引:《大叔手记全集》
大叔手记:旨在记录日常工作中的各种小技巧与资料(包括但不限于技术),如对你有用,请推荐一把,给大叔写作的动力