Friday, 5 February 2016

Attributes in C#

Introduction

Attributes are a new kind of declarative information. We can use attributes to define both design-level information (such as help file, URL for documentation) and run-time information (such as associating XML field with class field). We can also create "self-describing" components using attributes. In this tutorial we will see how we can create and attach attributes to various program entities, and how we can retrieve attribute information in a run-time environment.

Definition

As stated in MSDN (ms-help://MS.MSDNQTR.2002APR.1033/csspec/html/vclrfcsharpspec_17_2.htm)
"An attribute is a piece of additional declarative information that is specified for a declaration." 

Using Pre-defined Attributes

There is a small set of pre-defined attributes present in C#. Before learning how to create our own custom attributes, we will first look at how to use those in our code.
using System;
public class AnyClass 
{
    [Obsolete("Don't use Old method, use New method", true)]
    static void Old( ) { }
   
    static void New( ) { }
   
    public static void Main( ) 
    {
        Old( );
    }
}
Take a look at the this example. In this example we use attribute Obsolete, which marks a program entity that should not be used. The first parameter is the string, which explain why the item is obsolete and what to use instead of this. In fact you can write any othere text here. The second parameter tells the compiler to treat the use of the item as an error. Default value is false, which means compiler generates a warning for this.
When we try to compile above program, we will get an error:
AnyClass.Old()' is obsolete: 'Don't use Old method,  use New method'

Developing Custom Attributes

Now we will see how we can develop our own attributes. Here is a small recipe to create our own attributes.
Derive our attribute class from System.Attribute class as stated in C# language specification (A class that derives from the abstract class System.Attribute, whether directly or indirectly, is an attribute class. The declaration of an attribute class defines a new kind of attribute that can be placed on a declaration) and we are done.
using System;
public class HelpAttribute : Attribute
{
}
Believe me or not we have just created a custom attribute. We can decorate our class with it as we did with obsolete attribute.
[Help()]
public class AnyClass
{
}
Note: it is a convention to use the word Attribute as a suffix in attribute class names. However, when we attach the attribute to a program entity, we are free not to include the Attribute suffix. The compiler first searches the attribute in System.Attribute derived classes. If no class is found, the compiler will add the word Attribute to the specified attribute name and search for it.
But this attribute does nothing useful so far. To make it little useful let add something more in it.
using System;
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Descrition_in)
    {
        this.description = Description_in;
    }
    protected String description;
    public String Description 
    {
        get 
        {
            return this.description;
                 
        }            
    }    
}
[Help("this is a do-nothing class")]
public class AnyClass
{
}
In above example we have added a property to our attribute class which we will query at runtime in last section.

Defining or Controlling Usage of Our Attribute

AttributeUsage class is another pre-defined class that will help us in controlling the usage of our custom attributes. That is, we can define attributes of our own attribute class.
It describes how a custom attribute class can be used.
AttributeUsage has three properties which we can set while placing it on our custom attribute. The first property is:

ValidOn

Through this property, we can define the program entities on which our custom attribute can be placed. The set of all possible program entities on which an attribute can be placed is listed in theAttributeTargets enumerator. We can combine several AttributeTargets values using a bitwise OR operation.

AllowMultiple

This property marks whether our custom attribute can be placed more than once on the same program entity.

Inherited

We can control the inheritance rules of our attribute using this property. This property marks whether our attribute will be inherited by the class derived from the class on which we have placed it.
Let's do something practical. We will place AttributeUsage attribute on own Help attribute and control the usage of our attribute with the help of it.
using System;
[AttributeUsage(AttributeTargets.Class), AllowMultiple = false, 
 Inherited = false ]
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        this.description = Description_in;
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.description;
        }            
    }    
}
First look at AttributeTargets.Class. It states that Help attribute can be placed on a class only. This implies that following code will result in an error:
AnyClass.cs: Attribute 'Help' is not valid on this declaration type. 
It is valid on 'class' declarations only.
Now try to put in on method
[Help("this is a do-nothing class")]
public class AnyClass
{
    [Help("this is a do-nothing method")]    //error
    public void AnyMethod()
    {
    }
} 
We can use AttributeTargets.All to allow Help attribute to be placed on any program entity. Possible values are: 
  • Assembly, 
  • Module, 
  • Class, 
  • Struct, 
  • Enum, 
  • Constructor, 
  • Method, 
  • Property, 
  • Field,
  • Event, 
  • Interface, 
  • Parameter, 
  • Delegate, 
  • All = Assembly | Module | Class | Struct | Enum | Constructor | Method | Property | Field | Event | Interface | Parameter | Delegate,
  • ClassMembers = Class | Struct | Enum | Constructor | Method | Property | Field | Event | Delegate | Interface )
Now consider AllowMultiple = false . This state that attribute cannot be placed multiple time.
[Help("this is a do-nothing class")]
[Help("it contains a do-nothing method")]
public class AnyClass
{
    [Help("this is a do-nothing method")]        //error
    public void AnyMethod()
    {
    }
}
It generates a compile-time error.
AnyClass.cs: Duplicate 'Help' attribute
Ok now discuss the last property. Inherited, indicates whether the attribute, when placed on a base class, is also inherited by classes that derive from that base class. If Inherited for an attribute class is true, then that attribute is inherited. However if Inherited for an attribute class is false or it is unspecified, then that attribute is not inherited.
Let suppose we have following class relationship.
[Help("BaseClass")] 
public class Base
{
}

public class Derive :  Base
{
}
We have four possible combinations:
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false ] 
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited =false ] 
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true ] 
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited =true ] 

First Case

If we Query (we will see later how to Query attributes of a class at run-time) Derive Class for Help attribute, we will not found it as the inherited attribute is set to false.

Second Case

Second case is no different as inherited attribute is set to false in this case also.

Third Case

To explain the third and fourth cases, let's add the same attribute to the derive class also.
[Help("BaseClass")] 
public class Base
{
}
[Help("DeriveClass")] 
public class Derive :  Base
{
}
Now if we Query about Help attribute, we will get the drive class attribute only as inherited is true but multiples are not allowed so the base class Help is overridden by the Derive class Help attribute.

Fourth Case

In fourth case we will get both attributes when we query our Derive class for Help attribute as both inheritance and multiples are allowed in this case.
Note: AttributeUsage attribute is only valid on classes derived from System.Attribute and bothAllowMultiple and Inherited are false for this attribute.

Positional vs. Named Parameters

Positional parameters are parameters of the constructor of the attribute. They are mandatory and a value must be passed every time the attribute is placed on any program entity. On the other hand Named parameters are actually optional and are not parameters of the attribute's constructor.
To explain it in more detail, let's add another property in our Help class.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false,
 Inherited = false)]
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        this.description = Description_in;
        this.verion = "No Version is defined for this class";
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.description;
        }
    }
    protected String version;
    public String Version
    {
        get 
        {
            return this.version;
        }
        //if we ever want our attribute user to set this property, 
        //we must specify set method for it 
        set 
        {
            this.verion = value;
        }
    }
}
[Help("This is Class1")]
public class Class1
{
}

[Help("This is Class2", Version = "1.0")]
public class Class2
{
}

[Help("This is Class3", Version = "2.0", 
 Description = "This is do-nothing class")]
public class Class3
{
}
When we Query Class1 for Help attribute and its properties, we will get:
Help.Description : This is Class1
Help.Version :No Version is defined for this class
As we didn't define any value for Version property, the value set in the constructor is used. If no value is defined then the default value of the type is used (for example: in case int the default value is zero).
Now, Querying Class2 will result
Help.Description : This is Class2
Help.Version :  1.0
Don't use multiple constructors for optional parameter. Instead, mark them as named parameters. We called them named because when we supply their value in the constructor, we have to name them. For example, in second class, we define Help.
[Help("This is Class2", Version = "1.0")]
In the AttributeUsage example, the ValidOn parameter is a Positional parameter and Inherited and AllowMultiple are named parameters.
Note: To set the value of named parameter in the constructor of the attribute, we must supply the set method for that property otherwise it will generate a compile time error:
'Version' : Named attribute argument can't be a read only property
Now what happen when we will query Class3 for Help attribute and its properties? The result is the same compile-time error.
'Desciption' : Named attribute argument can't be a read only property
Now modify the Help class and add the set method of Description. Now the output will be:
Help.Description : This is do-nothing class 
Help.Version : 2.0
What happen behind the scene is first the constructor is called with positional parameters, and then set method is called for each named parameter. The value set in the constructor is override by the set method.

Parameters Types

The types of parameters for an attribute class are limited to:
  • bool
  • byte, 
  • char
  • double
  • float,
  • int
  • long
  • short
  • string 
  • System.Type 
  • object 
  • An enum type, provided that it and any types in which it is nested are publicly accessible. A one-dimensional array involving any of the types listed above 

Attributes Identifiers

Let's suppose, we want to place Help attribute on entire assembly. The First Question arises where to place that Help attribute so that compiler would determine that it is placed on an entire assembly?? Consider another situation; we want to place an attribute on the return type of a method. How compiler would determine that we are placing it on a method-return-type and not on entire method??
To resolve such ambiguities, we use attribute identifier. With the help of attribute identifiers, we can explicitly state the entity on which we are placing the attribute.
For example:
[assembly: Help("this a do-nothing assembly")]
The assembly identifier before the Help attribute explicitly tells the compiler that this attribute is attached to entire assembly. The possible identifiers are 
  • assembly
  • module
  • type
  • method
  • property
  • event
  • field
  • param
  • return

Querying Attributes at Run-Time

We have seen how to create attributes and how to attach them to a program element. Now its time to learn how user of our class can query this information at run-time.
To query a program entity about its attached attributes, we must use reflection. Reflection is the ability to discover type information at run time.
We can use the .NET Framework Reflection APIs to iterate through the metadata for an entire assembly and produce a list of all classes, types, and methods that have been defined for that assembly.
Remember the old Help attribute and the AnyClass class.
using System;
using System.Reflection;
using System.Diagnostics;

//attaching Help attribute to entire assembly
[assembly : Help("This Assembly demonstrates custom attributes 
 creation and their run-time query.")]

//our custom attribute class
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        //
        // TODO: Add constructor logic here
        this.description = Description_in;
        //
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.deescription;
                 
        }            
    }    
}
//attaching Help attribute to our AnyClass
[HelpString("This is a do-nothing Class.")]
public class AnyClass
{
//attaching Help attribute to our AnyMethod
    [Help("This is a do-nothing Method.")]
    public void AnyMethod()
    {
    }
//attaching Help attribute to our AnyInt Field
    [Help("This is any Integer.")]
    public int AnyInt;
}
class QueryApp
{
    public static void Main()
    {
    }
}
We will add attribute-query code to our Main method in the next two sections.

Querying Assembly Attributes

In the following code, we get the current process name and load the assembly using LoadFrom method of Assembly class. Then we use GetCustomAttributes method to get all the custom attributes attached to the current assembly. Next foreach statement iterate through all the attributes and try to the cast the each attribute as Help attribute (casting objects using the as keyword has an advantage that if the cast is invalid, we don't have to worry about an exception being thrown. What will happen instead is that the result will be null). The next line check if cast is valid and not equal to null then it display the Help property of the attribute.
class QueryApp
{
    public static void Main()
    {
        HelpAttribute HelpAttr;

        //Querying Assembly Attributes
        String assemblyName;
        Process p = Process.GetCurrentProcess();
        assemblyName = p.ProcessName + ".exe";

        Assembly a = Assembly.LoadFrom(assemblyName);

        foreach (Attribute attr in a.GetCustomAttributes(true))
        {
            HelpAttr = attr as HelpAttribute;
            if (null != HelpAttr)
            {
                Console.WriteLine("Description of {0}:\n{1}", 
                                  assemblyName,HelpAttr.Description);
            }
        }
}
}
The output of the following program is:
Description of QueryAttribute.exe:
This Assembly demonstrates custom attributes creation and 
their run-time query.
Press any key to continue

Querying Class, Method and Field Attributes

In the code below, the only unfamiliar is the first line in Main method.
Type type = typeof(AnyClass);
It gets the Type object associated with our AnyClass class using typeof operator. The rest of the code for querying class attributes is similar to the above example and don't need any explanation (I think).
For querying method's and field's attributes, we first get all the methods and fields present in the class then we query their associated attributes in the same manner as we did class attributes
class QueryApp
{
    public static void Main()
    {

        Type type = typeof(AnyClass);
        HelpAttribute HelpAttr;


        //Querying Class Attributes
        foreach (Attribute attr in type.GetCustomAttributes(true))
        {
            HelpAttr = attr as HelpAttribute;
            if (null != HelpAttr)
            {
                Console.WriteLine("Description of AnyClass:\n{0}", 
                                  HelpAttr.Description);
            }
        }
        //Querying Class-Method Attributes  
        foreach(MethodInfo method in type.GetMethods())
        {
            foreach (Attribute attr in method.GetCustomAttributes(true))
            {
                HelpAttr = attr as HelpAttribute;
                if (null != HelpAttr)
                {
                    Console.WriteLine("Description of {0}:\n{1}", 
                                      method.Name, 
                                      HelpAttr.Description);
                }
            }
        }
        //Querying Class-Field (only public) Attributes
        foreach(FieldInfo field in type.GetFields())
        {
            foreach (Attribute attr in field.GetCustomAttributes(true))
            {
                HelpAttr= attr as HelpAttribute;
                if (null != HelpAttr)
                {
                    Console.WriteLine("Description of {0}:\n{1}",
                                      field.Name,HelpAttr.Description);
                }
            }
        }
    }
}
The output of the following program is.
Description of AnyClass:
This is a do-nothing Class.
Description of AnyMethod:
This is a do-nothing Method.
Description of AnyInt:
This is any Integer.
Press any key to continue

No comments:

Post a Comment