Thursday, January 17, 2008

Working with Attributes

Working with Attributes

Attributes are used to specify additional information about an entity. This information gets written in metadata at compile time. An entity to which an attribute can be applied is known as the ‘attribute target’. The target can be an assembly, interface, structure, class and all possible class members. An attribute is applied to the target by specifying the attribute name in brackets as shown below.

[Serializable]

This attribute is applied to a class stating that objects of this class can be serialised. When the compiler encounters this attribute, it adds suitable instructions in the metadata.

There are thousands of pre-defined attributes in FCL. In addition to them .NET allows us to define our own attributes, called ‘custom attributes’. In this article we will examine the process of defining and using custom attributes and reading them using reflection.

We will create a custom attribute that can be applied to create a documentation of a class and its members. Documentation of a class would include the purpose of the class and how many methods it has, whereas documentation of method would include their purpose. We can write comments to achieve this. However, comments remain in the source code and are not accessible through the assembly. If we add documentation through attributes they go to the assembly and can be accessed using reflection.

Declaring an Attribute
As everything in C# is declared in a class, an attribute is also embodied in a class. To create a custom attribute we have to derive a class from the System.Attribute class. We have named our derived class as DocumentAttribute. The class DocumentAttribute has three data members as shown below.

class DocumentAttribute:System.Attribute
{
                        string info;
                        int cnt;
}

The string variable info keeps a description of the entity and the integer variable cnt keeps count of the methods. We can declare attributes either with parameters or without parameters. The parameters can be positional parameters or named parameters. The positional parameters are initialised in constructor, whereas, named parameters are initialised by defining a property. Another difference between the two is that positional parameters must be passed in the same sequence as given in the constructor. The named parameters are optional and can be passed in any sequence. We have declared the Document attribute to take two parameters. We want that one of them viz. info should be the positional parameter and cnt should be the named parameter. This is because cnt would be used only if attribute is applied to class. For class members we can drop it. So, we must define a constructor and a property as shown below.

public DocumentAttribute (string i)
{
            info = i ;
}
 
public int Count
{
            get
   {
                        return cnt ;
            }
   set
            {
                        cnt = value ;
            }
}

As we know, custom attributes are written in metadata and can be read through reflection. Generally speaking, reading an attribute means reading values of data members of the attribute class. If we try to read the Document attribute it won’t be able to access the info data member as it is declared private. So, in order to expose it through metadata, we would write a read-only property. Here it is:

public string Info
{
            get
            {
                        return info ;
            }
}

We must also mention the attribute targets. This can be done by using an attribute called AttributeUsage. The AttributeUsage attribute targets classes. So, it has to be declared just above the DocumentAttribute class as shown below.

[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method | 
AttributeTargets.Constructor | AttributeTargets.Field, 
AllowMultiple = false )]
 
class DocumentAttribute : System.Attribute
{
}

The AttributeUsage attribute takes two parameters. Using the first parameter we can specify attribute targets. By setting the second parameter to true we can specify that one member can have multiple Document attributes. When attribute targets are assembly or module, the attributes should be placed immediately after all using statements and before any code.

Now that our attribute has been readied, let us see how to use it.

Using an Attribute
We would use the Document attribute to create documentation of the class test1. Here is the declaration of test1 class.

[Document (“Class test1: created for testing custom attributes”, Count = 2 )]
class test1
{
            [Document ( “i: Counter variable” )]
            int i = 0;
            [Document ( “Ctor test1: zero-argument ctor” )]
            public test1()
            {
            }
            [Document ( “increment( ): Increments counter” )]
            public void increment()
            {
                        i++ ;
            }
            [Document ( “decrement( ): Decrements counter” )]
            public void decrement()
            {
                        i--;
            }
}

We have applied the Document attribute to the class through the statement

[Document (“Class test1: created for testing custom attributes”, Count=2 )]

Note that although name of our class is DocumentAttribute we have omitted the word Attribute here—the compiler would add it in automatically. When the compiler encounters this statement, it searches for a class derived from the System.Attribute class in all the namespaces mentioned in using statements.

The positional parameters are passed as the normal parameters, but named parameters are passed by mentioning the name of the parameter like

Count = 2

Needless to say this would invoke the set block of Count property. On similar grounds, we have applied our attribute to other class members.

Reading Attributes
The following code snippet shows how to read the Document attribute applied to a class.

Type t=typeof(test1);
object[] a= t.GetCustomAttributes(typeof (DocumentAttribute), false); 
foreach (object att in a)
{
                        DocumentAttribute d=(DocumentAttribute) att;
                        Console.WriteLine (“{0} {1}”, d.Info, d.Count);
}

To begin with we have created a reference t to the Type object. We have used the typeof operator on test1 class, which returns reference to an object of Type. The GetCustomAttributes( ) method takes the type of the attribute we want to search and returns an array of references to objects, each of type DocumentAttribute. Next, we have run a foreach loop to read and display each attribute in the array. In our case only one attribute would get displayed.

To read the attributes for methods, we would first obtain all the methods and then read attributes of each of them. This is done through the following statements.

MethodInfo[] mi=t.GetMethods();
foreach (MethodInfo m in mi)
{
            object[ ] o = m.GetCustomAttributes ( false ) ;
   foreach (object ma in o)
   {
                        DocumentAttribute d = ( DocumentAttribute ) ma ;
                        Console.WriteLine ( “{0}”, d.Info ) ;
            }
}

We have used the reference t to obtain the methods and collected them in an array of type MethodInfo. We have called the GetCustomAttributes( ) method of the MethodInfo class to read the attributes of each method.
Similarly we can use the GetCustomAttributes( ) method of the ConstructorInfo and FieldInfo classes to read the attributes given to constructors and fields.

0 Comments: