Custom Attributes


If you take a look at the IL generated when compiling your programs, you'll notice a lot of keywords and special flags that the CLR understands. By understand, I just mean that the CLR changes its execution behavior in some fashion based on the presence of, absence of, or flags associated with one of these keywords. Such things can appear for assemblies, types, members — just about any unit of reuse that can be represented in IL. We discussed many of these in Chapters 2 and 3.

By now, you should understand how and why you can interact with the CLR's type system metadata. You can access any of these special flags using the XxxInfo APIs discussed above. But what happens when you want to customize behavior on some new type of attribute? Say that you wanted to annotate types and their members with database information so that you could map between objects and rows in a database table. You would have a few options, such as:

  • Require types to inherit from a special type, say DatabaseObject, and override members that return a hash table of database-to-object mapping information.

  • Store mapping information in an external configuration file and load it at runtime to determine how to map between objects and database rows. This might be a simple XML file or perhaps some records in a database table.

There are certainly other options you could pursue. But why not take advantage of the rich metadata subsystem that the CLR has to offer?

Declaring Custom Attributes

A custom attribute (CA) permits you to do just that. You can create your own "keywords" or "flags" with which users can annotate their CTS abstractions. All you need to do is inherit from System.Attribute:

 class MyAttribute : Attribute { /* ... */ } 

And then users can decorate their code using your attribute:

 [assembly: MyAttribute] [module: MyAttribute] [MyAttribute] class UserClass {     [MyAttribute]     private int x;     [return: MyAttribute]     [MyAttribute]     public int IncrementX()     {         return x++;     }     public int IncrementXBy([MyAttribute] int y)     {         return x += y;     } } 

What does this actually do? The answer is nothing, really! But other code can now be written that recognizes MyAttribute and does something dynamically when it is present. This is perfect for the database situation described above, where some persistence manager type might recognize attributes on a type, and extract the data and perform the object-to-relational mapping based on that information.

By convention, all CAs are named with an Attribute suffix. Most languages permit you to leave off the trailing Attribute as a convenience when referring to the type. For example, UserClass above could have applied MyAttribute as follows:

 [My] class UserClass { /* ... */ } 

MyAttribute is probably a poor name for this reason. ([My] is not entirely informative as to the purpose of the attribute.) Most users will apply their attributes in this manner, so it is recommended that you name them with this in mind.

Marker Attributes

The MyAttribute type shown above is what is called a marker attribute. Marker attributes have no properties and thus no per-instance tate, but can still be useful to indicate to dynamic code that it must do something special. Empty interfaces can be used for similar situations, for example interface IMyInterface { }, but there is an important tradeoff being made.

As you will see momentarily, custom attributes have no runtime cost on the common CLR data structures when they aren't used. They are fairly expensive to extract, however. Interfaces, on the other hand, require additional slots in the most commonly accessed type and method descriptor structures that the runtime uses. Because of this, they are cheaper to check for existence on a type. The tradeoff is based on your common scenarios. If you intend to check a whole lot of objects for MyAttribute, using an interface would be a better solution. But if most objects don't pass through the code path that checks for these — as is usually the case — then using an interface just bloats common data structures.

Attributes with Properties

Most CAs are not marker attributes. To create an attribute with state, you simply add a set of publicly settable properties. Any public constructors can be used by clients to set the internal fields, in addition to using the property setter syntax in your favorite language. A consumer can use any one of your constructors, plus any combination of property setters in any order. In reality, the constructor- and property-based explanation is not accurate — all that is needed to set state on a CA is to serialize it correctly in the metadata (all attributes are Serializable). For most people that are working in C#, this describes a mental model that is close enough to work with attributes.

 class TableAttribute : Attribute {     public TableAttribute(string name)     {         this.name = name;     }     private string name;     private string dbName;     public string Name     {         get { return name; }         set { name = value; }     }     public string DbName     {         get { return dbName; }         set { name = value; }     } } 

This enables users to annotate their code with TableAttributes. With this definition, these attributes must be provided with a name parameter, and optionally can set the DbName property. The syntax C# offers is very much like instantiating an object. Although in reality, no object gets instantiated until somebody asks for it using GetCustomAttributes.

We might use this attribute as follows:

 [Table("customers", DbName=" Northwind")] class UserClass { /* ... */ } 

The setting of DbName is entirely optional. But because we didn't offer a no-args constructor, the user must supply the name argument. If we supplied a public TableAttribute() { } constructor, the user could annotate their type as follows:

 [Table(Name=" customers", DbName=" Northwind")] class UserClass { /* ... */ } 

The order in which the properties are set does not matter, and in fact we could have omitted them entirely due to the presence of the no-args constructor.

Attribute Usage

A new CA can be applied to any granularity of metadata by default. For example, you might annotate an assembly, type, member, and so on. This unit of granularity is commonly referred to as a target. Often it makes sense to restrict an attribute so that it can only be applied to a subset of possible targets. For example, a TableAttribute probably only makes sense on a type, assuming the model is such that types map one-to-one with tables in a database. It turns out that you can customize the type system and compiler's treatment of attributes by forbidding annotations on certain targets (or more accurately, by declaring which targets are legal).

The way you do this is by applying an attribute to your CA. When you apply AttributeUsage to your attribute, you declare the allowable targets by specifying AttributeTargets enum values: All, Assembly, Module, Class, Struct, Interface, Delegate, Enum, Constructor, Field, Property, Event, Method, Parameter, ReturnValue, and/or GenericParameter. I say "and/or" because this is a flags-style enum. Using a logical |, you can declare that the attribute is valid on a range of targets.

AttributeUsageAttribute offers a constructor that takes a set of targets as its first argument. For instance, say we wanted to restrict our TableAttribute to classes, structs, and interfaces only:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |                 AttributeTargets.Interface)] class TableAttribute : Attribute { /* ... */ } 

This will prevent clients of your attribute from decorating anything but classes, structs, and interfaces.

In addition to specifying the target, there are two other usage options you can change. By default, subclasses of a type decorated with your attribute will inherit the attribute. In other words, consider what occurs if a customer writes the following code:

 [MyAttribute] class A { /* ... */ } class B : A { /* ... */ } 

Unless you set the Inherited Boolean property with the AttributeUsageAttribute to false, when B is queried at runtime as to whether it is decorated with MyAttribute, it will report back yes. In many cases this is desirable — hence true being the default — but in others, it can result in incorrect behavior. For example, if we had a table mapping defined using CAs based on a subclass's data, we might try to incorrectly map a subclass later on. This could result in some fields that the subclass added not being set or perhaps some form of runtime error.

Lastly, the default behavior of CAs is that only one instance may appear on a specific target. In this sense, target doesn't mean type but rather a single instance of a type. In other words, it doesn't mean that only one type in your program can be annotated with MyAttribute but rather that a type A can only have one MyAttribute decorating it. To change this, set AllowMultiple to true using the AttributeUsageAttribute:

 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Field |                 AttributeTargets.Property | AttributeTargets.ReturnValue,                 true, false)] class MyAttribute { /* ... */ } 

In this bit of code, we declare that MyAttribute can be applied to methods, fields, properties, and return values. In addition to that, AllowMultiple is set to true (the second argument), and Inherited is set to false (the third argument).

Decorating Unconventional Targets (Language Feature)

You probably noticed three AttributeTargets in particular that seem like they'd be difficult to represent in source. Assembly, Module, and ReturnValue all require special syntax to declare what they are applied to. They look as follows:

 [assembly: MyAttribute] [module: MyAttribute] class Foo {     [return: MyAttribute]     public int Bar() { /* ... */ } } 

In C#, you use the assembly:, module:, and return: qualifiers to indicate the target.

Accessing Custom Attributes

As we've already noted, custom attributes don't buy you very much unless there is code that changes its behavior based on the presence or absence of them. There are plenty of examples in the runtime of this, for example AssemblyVersionAttribute, FlagsAttribute, SerializableAttribute, Struct LayoutAttribute, and STAThreadAttribute, just to name a few from mscorlib.dll. Each one of these attributes causes a piece of code in the CLR or Framework to change behavior.

But you can programmatically access attributes with the GetCustomAttributes method. This method is available on all of the info APIs outlined above in addition to types such as Assembly and Module. For example, you can inspect a Type for the presence of a specific attribute as follows:

 Type t = typeof(Foo); object[] attributes = t.GetCustomAttributes(typeof(MyAttribute), true); foreach (object attribute in attributes)     // ... 

Each custom attribute is instantiated and returned inside the object[] so that you can cast it to the appropriate attribute type and inspect its contents.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net