« Code Camp Auckland 2009 | Main | Sunday “cats in domestic appliances” blogging »

September 02, 2009

The case of the missing custom attributes

If you’ve used .NET a lot, you probably haven’t come across the Attribute.TypeId property.  This is the key – quite literally – to a problem that doesn’t come up very often – and by “not very often” I mean today I came across it for the second time in eight years – but should never have come up at all and is quite baffling when it does.

Let’s review.  .NET allows you to define your own attributes.  These attributes can be applied to types, methods, properties, and so on.  You can get at these attributes using reflection.  Life, in short, is good, at least for people whose desires extend no further than defining custom attributes and retrieving them again.

But .NET also has a sort of shadow type system: an early, hulking kind of dynamic type system tacked onto the side of the .NET 1.0 static type system.  It dwells in the System.ComponentModel namespace, TypeDescriptor is its herald, and ICustomTypeDescriptor is its tyrant king (and TypeConverter its capering fool).  The purpose of the shadow type system is to allow types to “adjust the truth,” adding properties that they don’t really have but are prepared to fake, and removing properties that they don’t want to talk about.  This is jolly useful.  It’s what supports, for example, making a Tooltip property appear in the Windows Forms property grid even though no control class actually has a Tooltip property, or applying Dynamic Data attributes to a property in one class by applying them to a field in a completely different class.

For 99.9% of the time – or, to be more exact, for 2920 days out of 2922 – the shadow type system works okay.  However, it does have some surprises up its sleeve, and one of those relates to custom attributes.  Specifically, if you apply more than one instance of the same custom attribute to a member, the shadow type system will randomly pick one of the instances, and throw the other one away.

Here’s an example.  Suppose we define a LocalDisplayNameAttribute class, which can be applied to a property to define a localised display name for a given culture:

[LocalDisplayName(“en-NZ”, “Sweet as”)]
[LocalDisplayName(“en-UK”, “Jolly good, old bean”)]
[LocalDisplayName(“en-US”, “WOO! YOU ROCK!”)]
public bool Yes { get; set; }

And now we ask the shadow type system for these attributes:

var prop = TypeDescriptor.GetProperties(type, “Huzzah”)[0];
var ldns = prop.Attributes.OfType<LocalDisplayNameAttribute>;
Console.WriteLine(ldns.Count());

This prints 1 rather than 3.  All instances of LocalDisplayNameAttribute except one have been discarded.

And this is where Attribute.TypeId comes in.

Attribute.TypeId has nothing to do with types, and very little to do with IDs.  It is misleadingly named, and misleadingly implemented.  Attribute.TypeId’s actual job is to answer the question, “Is this attribute instance a duplicate of this other attribute instance?”

Now the real type system has no problem with duplicate attribute instances.  It just stores the attributes and serves them up on demand, without passing judgment.  Hey, you wanted duplicates, you can have ’em.

The shadow type system, on the other hand, hates duplicates and makes sure to remove them during its process of “adjusting the truth.”  And it uses the TypeId as the criterion for doing so.  (Specifically, it uses TypeId as a dictionary key.)  And – and this is the kicker – the default implementation for TypeId is to return the attribute type.  Consequently, by default, the shadow type system sees all instances of the same attribute type as duplicates to be winnowed down to one.  In the example above, the shadow type system decided that the three LocalDisplayNameAttribute instances were duplicates and threw out all but one.

So, finally, we come to the fix.

If you’re creating a custom attribute which allows multiple applications, you almost certainly want to override TypeId to return a unique value for each attribute instance.  This could be a smart implementation which returns some appropriate combination of the attribute data, or it could just return a new GUID or even a this reference.

In the LocalDisplayNameAttribute example, as far as we’re concerned, there should be only one instance per culture ID.  If someone insists on supplying multiple local display names for the same culture:

[LocalDisplayName(“en-NZ”, “Sweet as”)]
[LocalDisplayName(“en-NZ”, “Choice”)]

then it’s choice and sweet as that the shadow type system recognises these as duplicates and dedupes them for us.  But we don’t want these en-NZ display names to come up as duplicates of other culture’s names and cause them to be discarded.  So our override of Attribute.TypeId looks like this:

public override object TypeId {
  get { return _cultureId; }
}

Parturient montes, nascetur ridiculus mus.  Well, better an easy fix than a hard one.

So what do you need to remember?

First, TypeId doesn’t identify a type.  It doesn’t really identify anything.  It answers the question, “are these two attribute instances duplicates?”

Second, if you’re seeing custom attributes go missing, TypeId is probably the source of the problem.

Third, and consequently, if you’re writing a custom attribute, and it allows multiple instances, you’ll probably want to override TypeId.

You won’t see this very often.  Unfortunately, when you do, the documentation is almost completely useless, and the default behaviour is more than a little surprising.  So file it away.  In 2013, you might just need it.

September 2, 2009 in Software | Permalink

TrackBack

TrackBack URL for this entry:
https://www.typepad.com/services/trackback/6a00d8341c5c9b53ef0120a596f792970c

Listed below are links to weblogs that reference The case of the missing custom attributes:

Comments

Good post Ivan! My first reaction was "why would i ever want to have multiple instances of the same attribute on a single method/property?". Your "LocalDisplayName" example was a good one, but of course it is flawed because you could just pass the string resource identifier in to the attribute and have it use the CurrentUICulture to retrieve the correct value.

I know it was just an example and i am being pedantic :) But that leaves the question: exactly why were you using multiple instances of the same attribute? I've never known anyone (till now) who ever needed to do this, so you've got me puzzled.

Posted by: slugster at Sep 3, 2009 11:20:07 AM

Here are a few examples of where you need multiple instances.

In the LightSpeed object-relational mapper (http://www.mindscape.co.nz/products/lightspeed/default.aspx), to control eager loading, you can specify that an association belongs to a named aggregate rather than being always being eager loaded or always lazy loaded. Now there might be multiple aggregates which eager-load a particular association. So you would need multiple instances of the EagerLoadAttribute to specify this.

Or in the framework, the XmlInclude attribute tells the XmlSerializer that a property of type SomeBase could actually be a SomeDerived. Now of course a base class might have multiple derived classes. So you need to be able to apply multiple XmlInclude attributes to a property to indicate that it might be a SomeDerived1, a SomeDerived2 or a SomeDerived3.

A final example, more relevant to the TypeDescriptor scenario, came up on MSDN at http://social.msdn.microsoft.com/Forums/en-US/winforms/thread/e6bb4146-eb1a-4c1b-a5b1-f3528d8a7864. This chap is using an attribute to control visibility in different circumstances. He doesn't say what these circumstances are, so I'll invent something: maybe he's displaying properties in a property grid-type scenario, and he needs to control who sees which ones. So his attribute says "this property should be displayed only if the user is a member of this role." So if a property is to be visible to members of multiple roles, he needs multiple instances of the attribute.

But it's fair to say that this requirement doesn't come up very often, especially in the UI context that the shadow type system is intended to support and that TypeId rears its ugly head in. That's why, despite the weird default behaviour, most people will still never need to know about this stuff!

Posted by: Ivan at Sep 3, 2009 11:53:24 AM

Omne tulit punctum qui miscuit utile dulci,lectorem delectando pariterque monendo.

...only you could get "sweet as" into an exposition on TypeId...

Posted by: Cath at Sep 24, 2009 7:21:54 AM