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.
Hide Copy 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:
Hide Copy Code
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.
Hide Copy Code
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.
Hide Copy Code
[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.
Hide Copy Code
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.
Hide Copy Code
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:
Hide Copy Code
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
Hide Copy Code
[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.
Hide Copy Code
[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.
Hide Copy Code
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.
Hide Copy Code
[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.
Hide Copy Code
[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.
Hide Shrink Copy Code
[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:
Hide Copy Code
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
Hide Copy Code
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.
Hide Copy Code
[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:
Hide Copy Code
'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.
Hide Copy Code
'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:
Hide Copy Code
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:
Hide Copy Code
[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.
Hide Shrink Copy Code
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.
Hide Copy Code
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:
Hide Copy Code
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.
Hide Copy Code
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
Hide Shrink Copy Code
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.
Hide Copy Code
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