前因
使用ASP.NET MVC时,我们必不可少的要与Attribute打交道,利用Attribute来做元数据的定义是一种非常老套的方法,但是相对于其方便快捷以及低廉的维护成本,还是在MVC框架中得到了充分的运用,主要是用于定义ModelMetadata。在现在的开发框架中,Attribute处理一个非常重要的地位。但是在使用Attribute过程,还是重复遇到了几回相同的问题,但由于缺少记录,让我重复花了好多时间来进行排查。
话说,我定义了一个Attribute类型,在一个类上面贴两个不同实例且属性值不同的两个Attribute,但是通过MVC的ModelMetadataProvider得到的却有一个实例。印象中曾经解决过该问题,但是始终无法记起是何原因还是花了很多时间去调试。其实如果幸运的话,可以通过网上找到答案,我们只要重写Attribute.TypeId这个属性,让它返回当前对象的实例即可:
public override object TypeId { get { return this; } }
分析
很简单的解决方法,但是究竟是何原因呢?还没有其它的更多的故事呢?我还可以进行以下的试验:
我们先定义一个Attribute类Attribute1 ,包含一个Name属性:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class Attribute1 : Attribute { public string Name { get; set; } }
接下来,我们再定义一个简单类型,给它贴上两个Attribute1的实例,分别给Name赋不同的值:
[Attribute1(Name = "a1")] [Attribute1(Name = "a2")] public class Class2 { }
接下来我们分别用两种方法来获取Attribute的实例:
- 使用标准的反射,用Attribute.GetCustomAttributes来获取自定义Attribute实例
- 使用TypeDescriptor.GetAttributes方法来获取Attributes实例,这也是MVC里面所使用的方法。
Console.WriteLine(Attribute.GetCustomAttributes(typeof(Class2), typeof(Attribute1)).Count()); Console.WriteLine(TypeDescriptor.GetAttributes(typeof(Class2)).Count);
我们的目的主是想要测试,我们通过两种方法所取得的Attribute实例的数量有何差别。当然我按照上面的代码,写一个简单的控制台代码,你得到的结果会是:2和1。也就是,我们通过标准的方法可能得到我们所预期的结果:2;但是通过TypeDescriptor,我们得到的却只有一个实例,重现了我们的问题。
继续下面的测试,我们回头修改一下Attributes1的定义,我们按照先前的解决方案,重写一下TypeId的实现:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class Attribute1 : Attribute { public string Name { get; set; } public override object TypeId { get { return this; } } }
不改面其它代码,运行输出测试结果,我们得到的结果会是:2和2。也就是它们得到的结果与我们的期望结果是一致的。如果此时停止测试,那么有可能在以后的使用过程当中我们还会重新陷入这个问题的泥潭,因为还有我们不一定知道的事情。
修改一下Class2的定义,我们让两个Attribute的实例的Name属性使用相同的值:
[Attribute1(Name = "a1")] [Attribute1(Name = "a1")] public class Class2 { }
此时的结果又变回了2和1了。看似也是个合理的结果,因为虽然是两个不同实例的Attribute1,但是它们实际所包含的信息确实是相同的,因此认为它们是相同的对象也不为过。但是这种理解却与我们对对象的相同性的认识却有不同,稍有不慎,还是会给我们带来一些麻烦。
结论
以上的试验告诉我们,如何可以避免类似问题的发生。虽然在MSDN的文档有说明,TypeId是Attribute的唯一标识,但是仅凭这一句话还是没有办法来解决我们所遇到的问题。翻开源码,我们来看看TypeId的实现:
public virtual object TypeId { get { return base.GetType(); } }
它用类型的实例来做为Attribute的唯一ID,这样虽然我们有不同的Attribute实例,但是得到的TypeId都是一样的。而当我们使用TypeDescriptor来读取Attributes时,它对根据TypeId对所有的Attribute进行一次过滤,滤去相同TypeId多个不同实例,只保留一个实例,类似于SQL的distinct关键字。更为隐蔽的是,它还会检查Attribute的所有的属性值,如果全部相同,它也会认为是相同的Attribute实例,而被过滤掉,而目前,我也还不知道如何解决这个问题。