The Weekly Source Code 54 - Can't have Multiple Attributes of the Same Type when using a TypeDescriptor
When I was in a China a few weeks back, I had a nice chat with a fellow named Zhe Wang who was using ASP.NET Dynamic Data to create a website. He made his own Custom Attribute to apply to his entities and effectively made an authorization scheme so that certain users could access some actions but not others.
In this little example, EntityAuthorize is Zhe's own custom attribute with a few properties.
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; }
}
}
In this example, he's got two attributes stacked up on the same class. There's nothing wrong with this, by the way.
Later on, when it came time to fetch these attributes and take action on them, something weird happened. He was only getting one of the attributes when he fetched them via MetaTable.Attributes.OfType<T>() which is the usual way in Dynamic Data. In frustration he tried to get them using GetCustomAttributes which is slow and not much fun. Here's his clever hack (note: this won't be needed as we'll solve this another way).
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);
}
How does MetaTable.Attributes get populated? Via the a standard System.ComponentModel.TypeDescriptor provider that does all the work of getting the attributes, merging them together and putting them in a collection. However, the TypeDescriptor is kind of goofy in this case due to a weird behavior deep in the system.
Here's a little reproduction application to show the behavior where the TypeDescriptor fails to get both attributes. The TypeDescriptor API has some subtle differences with the standard 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) { }
}
Notice the explicit AllowMultiple = true in the AttributeUsage attribute. However, deep in Attribute (yes, System.Attribute) there's a virtual property called TypeID and the value of that property becomes a key in the hashtable that the TypeDescriptor uses to remove duplicates. Inside of Attribute, we see:
public virtual object TypeId
{
get
{
return base.GetType();
}
}
Which probably should be something like this, however it's not (psudocode):
public virtual TypeID {
get {
if (IsAllowMultiple()) return this;
return GetType();
}
}
Since it's not really this way, but it IS virtual, you can certainly change your implementation of TypeID to return this, like, ahem, this:
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class TestPermissionAttribute : Attribute {
public TestPermissionAttribute(string role, string user) { }
public override object TypeId { get { return this; } }
}
However, in this naive example, this object has no state. Notice the parameters are not stored, so "this" would be the same object. Add a few fields so the objects are actually different as they now hold some states, and the sample works returning this as the 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; } }
}
So, the moral is, if you're using TypeDescriptors to get CustomAttributes and you want to have multiple attributes of the same type, make sure you:
- Override TypeId to return this
- Make sure AllowMultiple = true
- Make sure your object has some fields (i.e. state)
Thanks to Zhe Wang and David Ebbo for the samples and their help.
I hope the two people who made it this far into the post and the one person who would ever hit this edge case appreciates this tale of obscurity.
If you like, follow me on Twitter: @shanselman
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Since you're returning an Attribute instance that will be used as a key in a dictionary, presumably you're also best off overriding GetHashCode (and Equals) for that Attribute.
Kent
However, the default implementation of Attribute.GetHashCode relies on Type.GetHashCode if the attribute does not contain any fields. This is why two instances of the same field-less attribute would have the same hash code. The same applies to Attribute.Equals---it relies on Type.Equals if the attribute has no fields. In other words, if the attribute has no fields, using it as a key to a hashtable is equivalent to using its Type object.
Sasha
We've been using EF and DD for two years and have been bothered by AllowMultiple not working properly for quite some time in the dozen or more attributes we've defined. Our work around has been to stuff multiple attributes in a single attribute parm set. Now we can toss all of that and do it right.
Thanks!
Comments are closed.