Using Attributes in C#

Environment: C#

Introduction

Attributes are a new kind of declarative information. We can use attributes to define both design-level information (such as a help file or a URL for documentation) and run-time information (such as associating an xml field with a class field). We also can 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. We use the attribute Obsolete, which marks a program entity that should not be used. The first parameter is the string, which explains why the item is obsolete and what to use instead. In fact, you can write any other text here. The second parameter tells the compiler to treat the use of the item as an error. The default value is false, which means the compiler generates a warning for this.

When we try to compile the preceding 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 the System.Attribute class as stated in the 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 an 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 a little more useful, let’s add something more in it.


using System;
public class HelpAttribute : Attribute
{
public HelpAttribute(String Description_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. We will query it at runtime in the last section.

Defining or Controlling the Usage of Our Attribute

AttributeUsage class is another pre-defined class that will help us control 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 that we can set when placing it on our custom attribute. These three properties are discussed in the following sections.

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 the AttributeTargets enumerator. We can combine several AttributeTargets values by 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 by 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 the AttributeUsage attribute on our 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 the AttributeTargets.Class. It states that the Help attribute can be placed on a class only. This implies that the 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 it in a 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 the 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 states that an attribute cannot be placed multiple times.


[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

Okay, now let’s 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, that attribute is inherited. However, if Inherited for an attribute class is false or it is unspecified, that attribute is not inherited.

Let’s 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 ]

These combinations are discussed in the following sections.

First Case

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

Second Case

The second case is not different; an 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 the Help attribute, we will get the drive class attribute only because inherited is true, but multiples are not allowed so the base class Help is overridden by the Derive class Help attribute.

Fourth Case

In the fourth case, we will get both attributes when we query our Derive class for the Help attribute because both inheritance and multiples are allowed in this case.

Note: The AttributeUsage attribute is only valid on classes derived from System.Attribute and both AllowMultiple and Inherited are false for this attribute.

Positional versus Named Parameters

Positional parameters are parameters of the attribute’s constructor. 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 to our Help class.


[AttributeUsage(AttributeTargets.Class,
AllowMultiple = false,Inherited = false)]
public class HelpAttribute : Attribute
{
public HelpAttribute(String Description_in)
{
this.description = Description_in;
this.version = “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.version = 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 the Help attribute and its properties, we will get:


Help.Description : This is Class1
Help.Version :No Version is defined for this class

Because we didn’t define any value for the Version property, the value set in the constructor is used. If no value is defined, the default value of the type is used (for example, in case int, the default value is zero).

Now, querying Class2 will result in the following:


Help.Description : This is Class2
Help.Version : 1.0

Don’t use multiple constructors for optional parameters. 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, such as the following message:


‘Version’ : Named attribute argument can’t be a read-only property

Now what happens when we query Class3 for the Help attribute and its properties? The result is the same compile-time error.


‘Description’ : 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 happens behind the scenes is first the constructor is called with positional parameters, and then the set method is called for each named parameter. The value set in the constructor is overriden by the set method.

Parameter Types

The types of parameters for an attribute class are limited to the following:


  • 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

Attribute Identifiers

Let’s suppose that we want to place the Help attribute on the entire assembly. The first question arises: Where should we place that Help attribute so that the compiler can 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 would the compiler determine that we are placing it on a method-return-type and not on an entire method??

To resolve such ambiguities, we use an 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 the 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 it’s time to learn how the 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.description;

}
}
}
//attaching the Help attribute to our AnyClass
[HelpString(“This is a do-nothing Class.”)]
public class AnyClass
{
//attaching the 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 the 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 the LoadFrom method of the Assembly class. Then, we use the GetCustomAttributes method to get all the custom attributes attached to the current assembly. Next, for each statement iterate through all the attributes and try to cast each attribute as a 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 checks whether the cast is valid and not equal to null; then it displays 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 following code, the only unfamiliar item is the first line in the Main method.


Type type = typeof(AnyClass);

It gets the Type object associated with our AnyClass class using the typeof operator. The rest of the code for querying class attributes is similar to the preceding example and doesn’t need any explanation (I think).

For querying a 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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read