每周源代码54 —— 用TypeDescriptor不能获取同一类型的多个属性
[原文发表时间] 2010-07-03 12:48 AM
几周前我在中国,与一个叫Zhe Wang的人交谈,他正用ASP.NET动态数据创建网站。他制定了自己的自定义属性以便用于他的实体,并有效的制定了授权方案,这样只有特定的用户可以进行一些操作,其他用户却不可以。
在这个例子中,EntityAuthorize是Zhe自定义的带有一些特性的属性。
namespace MyModels.Metadata
{
[EntityAuthorize(Roles = "Administrators", AllowedEntityActions = EntityActions.List | EntityActions.Edit)]
[EntityAuthorize(Users = "Administrator", DeniedEntityActions = EntityActions.Edit)]
public partial class SurveyMetaData
{
string Address { get; set; }
string Content { get; set; }
string Something { get; set; }
EntityCollection<OtherStuff> OtherThings { get; set; }
}
}
在这个例子中,他把两个属性放在了一个类中。顺便说一下,这没有什么不对。
后来,当要获取这些属性并处理的时候,奇怪的事情发生了。当他通过Dynamic Data中的常用方式MetaTable.Attributes.OfType<T>()来获取属性的时候,他只能得到一个。失望中他试图用很慢并且不好玩的GetCustomAttributes来获取。这是他很机智的解决方法(注意:这是没必要的,因为我们会用另一种方法解决这个问题)。
public static bool FilterVisible(this MetaTable table, EntityActions action)
{
var usn = HttpContext.Current.User.Identity.Name;
var roles = Roles.GetRolesForUser(usn);
var attrs = table.Attributes.OfType<EntityAuthorizeAttribute>()
.Where
(
//try to get the attributes we need using the DynamicData metatable,
// but we're only getting one of the attributes of this type.
).Union
(
//get it ourselves the slow way using GetCustomAttributes and it works.
table.Attributes.OfType<MetadataTypeAttribute>()
.Select(t => Attribute.GetCustomAttributes(t.MetadataClassType))
.SelectMany(col => col).OfType<EntityAuthorizeAttribute>()
);
var allow = attrs.Any(a => a.AllowedEntityActions.HasFlag(action));
var deny = attrs.Any(a => a.DeniedEntityActions.HasFlag(action));
return allow && (!deny);
}
MetaTable.Attributes是如何流行的?通过一个标准的System.ComponentModel.TypeDescriptor提供程序,该程序获取所有的属性,合并在一起并放在集合里。但是,在这种情况下TypeDescriptor有点不是很好,因为系统内部有个异常的行为。
这有个简单的复制程序,可以显示TypeDescriptor未能获取两个属性的行为。TypeDescriptor API与标准的reflection API有些细微的差别。
class Program
{
static void Main(string[] args)
{
var prop = TypeDescriptor.GetProperties(typeof(Product))["Num"];
foreach (Attribute attrib in prop.Attributes)
{
Console.WriteLine(attrib.GetType());
}
}
}
public class Product
{
[TestPermission("foo1", "bar1")]
[TestPermission("foo2", "bar2")]
public int Num { get; set; }
}
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class TestPermissionAttribute : Attribute
{
public TestPermissionAttribute(string role, string user) { }
}
注意在trueAttributeUsage属性中,显性的AllowMultiple=true。但是,在Attribute(是的,System.Attribute)内部,有个叫TypeID的虚拟属性,属性的值在哈希表中变成了键值,并被TypeDescriptor用来删除重复值。在Attribute内部,我们可以看到:
public virtual object TypeId
{
get
{
return base.GetType();
}
}
可能应该是这样的一些类似代码,但不是(伪码):
public virtual TypeID {
get {
if (IsAllowMultiple()) return this;
return GetType();
}
}
因为不是真正的这样,但它是虚拟的,你当然可以改变你的TypeID执行,返回像如下这样:
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class TestPermissionAttribute : Attribute
{
public TestPermissionAttribute(string role, string user) { }
public override object TypeId { get { return this; } }
}
但是,在这个简单的例子中,对象没有状态。注意参数没有被存储,所以“这”会是同样的对象。添加一些字段,对象就不同了,因为现在他们有一些状态,例程返回如下TypeID:
using System;
using System.ComponentModel;
class Program
{
static void Main(string[] args)
{
var prop = TypeDescriptor.GetProperties(typeof(Product))["Num"];
foreach (Attribute attrib in prop.Attributes)
{
Console.WriteLine(attrib.GetType());
}
Console.ReadLine();
}
}
public class Product
{
[TestPermission("foo1", "bar1")]
[TestPermission("foo2", "bar2")]
public int Num { get; set; }
}
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class TestPermissionAttribute : Attribute
{
public TestPermissionAttribute(string role, string user) { Role = role; User = user; }
private string Role;
private string User;
public override object TypeId { get { return this; } }
}
所以,正确的方法是,如果你用TypeDescriptors获取CustomAttributes,并且你想要同一类的多个属性,你要确保:
重载TypeId以便返回this
确保AllowMultiple = true
确保你的对象有一些字段(即状态)
感谢Zhe Wang 和 David Ebbo的示例和帮助。
我希望这两个人(他们使得这个文章能更加深入)以及曾经遇到到了类似情况的人会喜欢这篇博客。
如果你感兴趣,可以Twitter我 : @shanselman