大叔手記(8):Interface Attributes != Class Attributes
2011-12-16 08:58 湯姆大叔 閱讀(3268) 評論(1) 收藏 舉報問題
事情來源于很早之前Team成員一個不規(guī)范的設(shè)計,在MVC3的項目上,由于所有的Model都需要有一些基本的名稱或者操作,加之應(yīng)用了DI,所以就想當(dāng)然地定義了一個接口,里面包含了一些接口屬性和方法,可突然有一天要求在這些屬性上應(yīng)用一些驗證約束和授權(quán),于是接口代碼改成了這樣:
public interface IModel
{
[Required]
string ModelName { get; set; }
[Permission(Configuration = "Debug")]
void OutputMessage();
}
實現(xiàn)類,幾乎沒有改變,只是在需要驗證的屬性上添加了Required類似的attribute:
public class SearchCriteria : IModel
{
public string ModelName { get; set; }
[Required]
public string Keyword { get; set; }
// 更多Model屬性
public void OutputMessage()
{
// 這里是處理代碼
}
}
可是后來應(yīng)用的時候,發(fā)現(xiàn)接口里定義的驗證約束根本不起作用,后來開始一路查找。
分析
看了MVC的源碼,找到了2個相關(guān)的地方。
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類實現(xiàn)了GetValidators抽象方法,節(jié)選代碼:
// 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));
}
可是從里面根本沒有發(fā)現(xiàn)有什么特殊處理的地方,那為何就不能把接口的Attributes取出來呢?
后來看到Brad Wilson的一篇文章,才發(fā)現(xiàn)原來CLR對于基類和接口的處理是不一樣的,基類是繼承的,所以所有基類里出現(xiàn)的方法,屬性等在子類實現(xiàn)類里具有同樣的效果,但是接口是不一樣的,接口是用來實現(xiàn)的,要求子類必須實現(xiàn),但接口有2種實現(xiàn)方式:隱式和顯式。
隱式:也就是我們上面的代碼使用的方式,用public的屬性和方法去實現(xiàn)接口,但是重要的,實現(xiàn)類里的public的屬性和方法和接口里聲明的那些完全不是一樣的東西,雖然他們有相同的簽名,通過反射代碼,我們可以看出兩者之間完全不一樣,因為這個不同,所以說接口屬性和方法上的metadata Attribute不會作用在實現(xiàn)類上,也就會出現(xiàn)我們文章開頭的問題。
這些規(guī)則其實是CLR定義好的,和MVC無關(guān),查看MVC里關(guān)于model綁定的源碼,我們可以知道,在進(jìn)行model綁定的時候,我們是基于實現(xiàn)類類型的,而不是接口類型,因為action里的參數(shù)一般來說都是實現(xiàn)類類型(MVC隱式創(chuàng)建),而不是接口類型(MVC實現(xiàn)不了),所以在上述2段MVC源碼里通過反射查找Attributes的時候只能查詢到實現(xiàn)類類型的Attributes。
改進(jìn)
知道了原因,我們就得改代碼了,第一種最簡單的方式就是在子類實現(xiàn)類上需要驗證的屬性上逐一添加相應(yīng)的驗證類型Attributes,后來覺得其實Model綁定也沒必要非要得DI掛上鉤,所以改成了抽象類,如下:
public abstract class ModelBase
{
[Required]
string ModelName { get; set; }
[Permission(Configuration = "Debug")]
void OutputMessage();
}
這樣,就不用在子類實現(xiàn)類里逐一添加這些Attributes了。
當(dāng)然,如果你非要使用接口,而又不想在子類里逐一添加Attributes,那恐怕你只有在MVC里使用自己的自定義ModelValidatorProvider了,在保留原來代碼的基礎(chǔ)上,加上一段特殊的邏輯,把該model所實現(xiàn)的接口逐一判斷一下,看看里面有沒有帶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
同步與結(jié)束語
本文已同步至目錄索引:《大叔手記全集》
大叔手記:旨在記錄日常工作中的各種小技巧與資料(包括但不限于技術(shù)),如對你有用,請推薦一把,給大叔寫作的動力
浙公網(wǎng)安備 33010602011771號