Attributes in C#.

In most programming languages, some information is expressed through declarations, and other information is expressed through code. For example, in the following class member declaration, the compiler and runtime will reserve space for an integer variable and set its protection so it’s visible everywhere:

public int Test;

This is an example of declarative information; it’s nice because of the economy of expression and because the compiler handles the details for you.

Typically, the types of declarative information are predefined by the language designer, and they can’t be extended by users of the language. A user who wants to associate a specific database field with a field of a class, for example, must invent a way of expressing that relation­ship in the language, a way of storing the relationship, and a way of accessing the information at runtime. In a language such as C++, the user might define a macro that stores the informa­tion in a field that’s part of the object. Such schemes work, but they’re error-prone and not generalized. They’re also ugly.

The .NET runtime supports attributes, which are merely annotations placed on elements of source code, such as classes, members, parameters, and so on. You can use attributes to change the behavior of the runtime, provide transaction information about an object, or convey organizational information to a designer. The attribute information is stored with the metadata of the element and can be easily retrieved at runtime through a process known as reflection.

C# uses a conditional attribute to control when member functions are called. The following is an example of the conditional attribute:

using System.Diagnostics; class Test {

[Conditional(“DEBUG”)]

public void Validate()

{

}

}

Most programmers will use predefined attributes much more often than they will write an attribute class.

1. Using Attributes

Suppose you’re doing a project with a group, and suppose it’s important to keep track of the code reviews that have been performed on the classes so you can determine when you’re done with them. You could store the code review information in a database, which would allow easy queries about status, or you could store it in comments, which would make it easy to look at the code and the information at the same time.

Or you could use an attribute, which would enable both kinds of access.

To do that, you need an attribute class. An attribute class defines the name of an attribute, how it can be created, and the information that will be stored. The “An Attribute of Your Own” section covers the gritty details of defining attribute classes.

The example attribute class looks like this:

using System;

[AttributeUsage(AttributeTargets.Class)]

public class CodeReviewAttribute: System.Attribute

{

public CodeReviewAttribute(string reviewer, string date)

{

this.reviewer = reviewer;

this.date = date;

}

public string Comment {

get

{

return(comment);

}

set

{

comment = value;

}

}

public string Date {

get

{

return(date);

}

}

public string Reviewer {

get

{

return(reviewer);

}

}

string reviewer;

string date;

string comment;

}

[CodeReview(“Eric”, “01-12-2000″, Comment=”Bitchin’ Code”)]

class Complex

{

}

The AttributeUsage attribute before the class specifies that this attribute can be placed only on classes. When an attribute is used on a program element, the compiler checks to see whether the use of that attribute on that program element is allowed.

The naming convention for attributes is to append Attribute to the end of the class name. This makes it easier to tell which classes are attribute classes and which classes are normal classes. All attributes must derive from System.Attribute.

The class defines a single constructor that takes a reviewer and a date as parameters, and it also has the public string property Comment.

When the compiler comes to the attribute used on the class Complex, it first looks for a class derived from Attribute named CodeReview. It doesn’t find one, so it next looks for a class named CodeReviewAttribute, which it finds.

Next, it checks to see whether the attribute is allowed for this usage (on a class).

Then, it checks to see if there’s a constructor that matches the parameters you’ve specified in the attribute use. If it finds one, an instance of the object is created—the constructor is called with the specified values.

If named parameters exist, it matches the name of the parameter with a field or property in the attribute class, and then it sets the field or property to the specified value.

After this is done, the current state of the attribute class is saved to the metadata for the program element for which it was specified.

At least, this is what happens logically. In actuality, it only looks like it happens this way; see the “Attribute Pickling” sidebar for a description of how it’s implemented.

2. A Few More Details

You can use some attributes only once on a given element. You can use others, known as multiuse attributes, more than once. They might be used, for example, to apply several different security attributes to a single class. The documentation on the attribute will describe whether an attribute is single-use or multiuse.

In most cases, it’s clear that the attribute applies to a specific program element. However, consider the following case:

using System.Runtime.InteropServices; class Test {

[return: MarshalAs(UnmanagedType.LPWStr)]

public static extern string GetMessage();

}

In most cases, an attribute in that position would apply to the member function, but this attribute is really related to the return type. How can the compiler tell the difference?

This can happen in the following situations:

  • Method vs. return value
  • Event vs. field or property
  • Delegate vs. return value
  • Property vs. accessor vs. return value of getter vs. value parameter of setter

For each of these situations, one case is much more common than the other case, and it becomes the default case. To specify an attribute for the nondefault case, you must specify the element to which the attribute applies:

using System.Runtime.InteropServices; class Test {

[return: MarshalAs(UnmanagedType.LPWStr)]

public static extern string GetMessage();

}

The return: indicates that this attribute should be applied to the return value.

You can specify the element even if there’s no ambiguity. Table 22-1 describes the identifiers.

Attributes that are applied to assemblies or modules must occur after any using clauses and before any code:

using System;

[assembly:CLSCompliant(true)]

class Test

{

Test() {}

}

This example applies the ClsCompliant attribute to the entire assembly. All assembly-level attributes declared in any file that’s in the assembly are grouped together and attached to the assembly.

To use a predefined attribute, start by finding the constructor that best matches the infor­mation to be conveyed. Next, write the attribute, passing parameters to the constructor. Finally, use the named parameter syntax to pass additional information that wasn’t part of the constructor parameters.

For more examples of attribute use, refer to Chapter 33.

3. An Attribute of Your Own

To define attribute classes and reflect on them at runtime, you have to consider a few more issues. The following sections will discuss some things to consider when designing an attribute.

You have two major things to determine when writing an attribute. The first is the program elements that the attribute may be applied to, and the second is the information that will be stored by the attribute.

3.1. Attribute Usage

Placing the AttributeUsage attribute on an attribute class controls where the attribute can be used. The possible values for the attribute are listed in the AttributeTargets enumerator (see Table 22-2).

As part of the AttributeUsage attribute, one of these can be specified or a list of them can be ORed together.

You can also use the AttributeUsage attribute to specify whether an attribute is single-use or multiuse. You do this with the named parameter AllowMultiple. Such an attribute looks like this:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Event,

AllowMultiple = true)]

3.2. Attribute Parameters

The information the attribute will store should be divided into two groups: the information that’s required for every use and the optional items.

The information that’s required for every use should be obtained via the constructor for the attribute class. This forces the user to specify all the parameters when they use the attribute.

Optional items should be implemented as named parameters, which allows the user to specify whichever optional items are appropriate.

If an attribute has several different ways in which it can be created, with different required information, separate constructors can be declared for each usage. Don’t use separate constructors as an alternative to optional items.

Attribute Parameter Types

The attribute pickling format supports only a subset of all the .NET runtime types, and there­fore, you can use only some types as attribute parameters. The types allowed are the following:

  • bool, byte, char, double, float, int, long, short, string
  • object
  • system.Type
  • An enum that has public accessibility (not nested inside something nonpublic)
  • A one-dimensional array of one of the previous types

4. Reflecting on Attributes

Once attributes are defined on some code, it’s useful to be able to find the attribute values. You can do this through reflection.

The following code shows an attribute class, the application of the attribute to a class, and the reflection on the class to retrieve the attribute:

using System;

using System.Reflection;

[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]

public class CodeReviewAttribute: System.Attribute {

public CodeReviewAttribute(string reviewer, string date)

{

this.reviewer = reviewer;

this.date = date;

}

public string Comment {

get

{

return(comment);

}

set

{

comment = value;

}

}

public string Date {

get

{

return(date);

}

}

public string Reviewer {

get

{

return(reviewer);

}

}

string reviewer;

string date;

string comment;

}

[CodeReview(“Eric”, “01-12-2000″, Comment=”Bitchin’ Code”)]

[CodeReview(“Gurn”, “01-01-2000″, Comment=”Revisit this section”)]

class Complex

{

}

class Test {

public static void Main()

{

Type type = typeof(Complex);

foreach (CodeReviewAttribute att in

type.GetCustomAttributes(typeof(CodeReviewAttribute), false))

{

CodeReviewAttribute att = (CodeReviewAttribute) atts[0];

Console.WriteLine(“Reviewer: {0}”, att.Reviewer);

Console.WriteLine(“Date: {0}”, att.Date);

Console.WriteLine(“Comment: {0}”, att.Comment);

}

}

}

The Main() function first gets the type object associated with the type Complex. It then iterates over all the CodeReviewAttribute attributes attached to the type and writes the values.

Alternately, the code could get all the attributes by omitting the type in the call to GetCustomAttributes:

foreach (object o in type.GetCustomAttributes(false))

{

CodeReviewAttribute att = o as CodeReviewAttribute;

if (att != null)

{

// write values here…

}

}

This example produces the following output:

Reviewer: Eric

Date: 01-12-2000

Comment: Bitchin’ Code

Reviewer: Gurn

Date: 01-01-2000

Comment: Revisit this section

The false value in the call to GetCustomAttributes tells the runtime to ignore any inherited attributes. In this case, it’ll ignore any attributes on the base class of Complex.

In the example, you can obtain the type object for the Complex type using typeof. You can also obtain it in the following manner:

Complex c = new Complex();

Type t = c.GetType();

Type t2 = Type.GetType(“Complex”);

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *