標籤:mvc
問題
事情來源於很早之前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://www.zuiniusn.com原創文章,未經博主允許不得轉載。
Interface Attributes != Class Attributes