Predefined, Reserved Attributes
More About Applying Attributes
Most programs are written to work on data. They read, write, manipulate, and display data. (Graphics are a form of data.) The types that you as the programmer create and use are designed for these purposes, and it is you, at design time, who must understand the characteristics of the types you use.
For some types of programs, however, the data they manipulate is not numbers, text, or graphics but information about programs and program types.
An object browser is an example of a program that displays metadata. It can read assemblies and display the types they contain, along with all the characteristics and members.
This chapter will look at how your programs can reflect on data using the Type
class and how you can add metadata to your types using attributes.
Note To use reflection, you must use the System.Reflection
namespace.
Throughout this text I've described how to declare and use the types available in C#. These include the predefined types (int
, long
, string
, and so on), types from the BCL (Console
, IEnumerable
, and so on), and user-defined types (MyClass
, MyDel
, and so on). Every type has its own members and characteristics.
The BCL declares an abstract class called Type
, which is designed to contain the characteristics of a type. Using objects of this class allows you to get information about the types your program is using.
Since Type
is an abstract class, it cannot have actual instances. Instead, at run time, the CLR creates instances of a class derived from Type
(RuntimeType
) that contains the type information. When you access one of these instances, the CLR returns a reference, not of the derived type but of the base class Type
. For simplicity's sake, though, throughout the rest of the chapter, I'll call the object pointed at by the reference an object of type Type
, although technically it's an object of a derived type that is internal to the BCL.
Important things to know about Type
are the following:
Type
object that contains the information about the type.Type
object.Type
object associated with all the instances.Figure 24-1 shows a running program with two MyClass
objects and an OtherClass
object. Notice that although there are two instances of MyClass
, there is only a single Type
object representing it.
Figure 24-1. The CLR instantiates objects of type Type for every type used in a program.
You can get almost anything you need to know about a type from its Type
object. Table 24-1 lists some of the more useful members of the class.
Table 24-1. Selected Members of Class System.Type
Member | Member Type | Description |
Name |
Property | Returns the name of the type. |
Namespace |
Property | Returns the namespace containing the type declaration. |
Assembly |
Property | Returns the assembly in which the type is declared. If the type is generic, it returns the assembly in which the type is defined. |
GetFields |
Method | Returns a list of the type's fields. |
GetProperties |
Method | Returns a list of the type's properties. |
GetMethods |
Method | Returns a list of the type's methods. |
There are several ways to get a Type
object. We'll look at using the GetType
method and using the typeof
operator.
Type object
contains a method called GetType
, which returns a reference to an instance's Type
object. Since every type is ultimately derived from object
, you can call the GetType
method on an object of any type to get its Type
object, as shown here:
Type t = myInstance.GetType();
The following code shows the declarations of a base class and a class derived from it. Method Main
creates an instance of each class and places the references in an array called bca
for easy processing. Inside the outer foreach
loop, the code gets the Type
object and prints out the name of the class. It then gets the fields of the class and prints them out. Figure 24-2 illustrates the objects in memory.
using System;
using System.Reflection; // Must use this namespace
class BaseClass
{ public int BaseField = 0; }
class DerivedClass : BaseClass
{ public int DerivedField = 0; }
class Program
{
static void Main( )
{
var bc = new BaseClass();
var dc = new DerivedClass();
BaseClass[] bca = new BaseClass[] { bc, dc };
foreach (var v in bca)
{
Type t = v.GetType(); // Get the type.
Console.WriteLine("Object type : {0}", t.Name);
FieldInfo[] fi = t.GetFields(); // Get the field info.
foreach (var f in fi)
Console.WriteLine(" Field : {0}", f.Name);
Console.WriteLine();
}
}
}
This code produces the following output:
Object type : BaseClass
Field : BaseField
Object type : DerivedClass
Field : DerivedField
Field : BaseField
Figure 24-2. The base class and derived class objects along with their Type objects
You can also use the typeof
operator to get a Type
object. Just supply the name of the type as the operand, and it returns a reference to the Type
object, as shown here:
The following code shows a simple example of using the typeof
operator:
using System;
using System.Reflection; // Must use this namespace
namespace SimpleReflection
{
class BaseClass
{ public int MyFieldBase; }
class DerivedClass : BaseClass
{ public int MyFieldDerived; }
class Program
{
static void Main( )
{
Type tbc = typeof(DerivedClass); // Get the type.
Console.WriteLine("Result is {0}.", tbc.Name);
Console.WriteLine("It has the following fields:"); // Use the type.
FieldInfo[] fi = tbc.GetFields();
foreach (var f in fi)
Console.WriteLine(" {0}", f.Name);
}
}
}
This code produces the following output:
Result is DerivedClass.
It has the following fields:
MyFieldDerived
MyFieldBase
An attribute is a language construct that allows you to add metadata to a program's assembly. It's a special type of class for storing information about program constructs.
Figure 24-3 is an overview of the components involved in using attributes and illustrates the following points about them:
Figure 24-3. The components involved with creating and using attributes
By convention, attribute names use Pascal casing and end with the suffix Attribute
. When applying an attribute to a target, you can leave off the suffix. For example, for attributes SerializableAttribute
and MyAttributeAttribute
, you can use the short names Serializable
and MyAttribute
when applying them to a construct.
The purpose of an attribute is to tell the compiler to emit a certain set of metadata about a program construct to the assembly. You do this by applying the attribute to the construct.
For example, the following code shows the headings of two classes. The first few lines of code show an attribute named Serializable
applied to class MyClass
. Notice that Serializable
has no parameter list. The second class declaration has an attribute called MyAttribute
, which has a parameter list with two string
parameters.
[ Serializable ] // Attribute
public class MyClass
{ ...
[ MyAttribute("Simple class", "Version 3.57") ] // Attribute with parameters
public class MyOtherClass
{ ...
Some important things to know about attributes are the following:
Before looking at how you can define your own attributes, this section describes two attributes predefined and reserved by .NET: the Obsolete
and Conditional
attributes.
The Obsolete
attribute allows you to mark a program construct as obsolete and to display a helpful warning message when the code is compiled. The following code shows an example of its use:
Notice that method Main
calls PrintOut
even though it's marked as obsolete. In spite of this, the code compiles and runs fine and produces the following output:
Start of Main
During compilation, though, the compiler produces the following CS0618 warning message to inform you that you're using an obsolete construct:
'AttrObs.Program.PrintOut(string)' is obsolete: 'Use method SuperPrintOut'
Another overload of the Obsolete
attribute takes a second parameter of type bool
. This parameter specifies whether use of the target should be flagged as an error instead of just a warning. The following code specifies that it should be flagged as an error:
The Conditional
attribute allows you to either include or exclude all the invocations of a particular method. To use the Conditional
attribute, apply it to the method declaration, along with a compilation symbol as a parameter.
The CIL code defining the method itself is always included in the assembly. It's just the invocations that are either inserted or omitted.
For example, in the following code, the Conditional
attribute is applied to the declaration of a method called TraceMessage
. The attribute has a single parameter, which in this case is the string DoTrace
.
DoTrace
defined.DoTrace
is defined, the compiler includes all the calls to method TraceMessage
, as usual.DoTrace
compilation symbol defined, it doesn't output code for any of the calls to TraceMessage
.The following code shows a full example of using the Conditional
attribute.
Main
contains two calls to method TraceMessage
.TraceMessage
is decorated with the Conditional
attribute, which has the compilation symbol DoTrace
as its parameter. So if DoTrace
is defined, the compiler will include the code for all the calls to TraceMessage
.DoTrace
, the compiler will include the code for both calls to TraceMessage
.#define DoTrace
using System;
using System.Diagnostics;
namespace AttributesConditional
{
class Program
{
[Conditional( "DoTrace" )]
static void TraceMessage(string str)
{ Console.WriteLine(str); }
static void Main( )
{
TraceMessage("Start of Main");
Console.WriteLine("Doing work in Main.");
TraceMessage("End of Main");
}
}
}
This code produces the following output:
Start of Main
Doing work in Main.
End of Main
If you comment out the first line so that DoTrace
is not defined, the compiler will not insert the code for the two calls to TraceMessage
. This time, when you run the program, it produces the following output:
Doing work in Main.
The .NET Framework predefines a number of attributes that are understood and interpreted by the compiler and the CLR. Table 24-2 lists some of these. The table uses the short names, without the “Attribute” suffix. For example, the full name of CLSCompliant
is CLSCompliantAttribute
.
Table 24-2. Important Attributes Defined in .NET
Attribute | Meaning |
CLSCompliant |
Declares that the publicly exposed members should be checked by the compiler for compliance with the CLS. Compliant assemblies can be used by any .NET-compliant language. |
Serializable |
Declares that the construct can be serialized. |
NonSerialized |
Declares that the construct cannot be serialized. |
Obsolete |
Declares that the construct should not be used. The compiler also produces a compile-time warning or error message, if the construct is used. |
DLLImport |
Declares that the implementation is unmanaged code. |
WebMethod |
Declares that the method should be exposed as part of an XML web service. |
AttributeUsage |
Declares what types of program constructs the attribute can be applied to. This attribute is applied to attribute declarations. |
The simple attributes shown so far have used a single attribute applied to a method. This section describes other types of attribute usage.
You can apply multiple attributes to a single construct.
For example, the following two sections of code show the two ways of applying multiple attributes. The sections of code are equivalent.
Besides classes, you can also apply attributes to other program constructs such as fields and properties. The following declaration shows an attribute on a field, and multiple attributes on a method:
[MyAttribute("Holds a value", "Version 3.2")] // On a field
public int MyField;
[Obsolete] // On a method
[MyAttribute("Prints out a message.", "Version 3.6")]
public void PrintOut()
{
...
You can also explicitly label attributes to apply to a particular target construct. To use an explicit target, place the target type, followed by a colon, at the beginning of the attribute section. For example, the following code decorates the method with an attribute and also applies an attribute to the return value.
The C# language defines ten standard attribute targets, which are listed in Table 24-3. Most of the target names are self-explanatory, but type
covers classes, structs, delegates, enums, and interfaces. The typevar
target name specifies type parameters to constructs that use generics.
event |
field |
method |
param |
property |
return |
type |
typevar |
assembly |
module |
You can also use an explicit target to set attributes at the assembly and module level, by using the assembly
and module
target names. (Assemblies and modules were explained in Chapter 10.) Some important points about assembly-level attributes are the following:
AssemblyInfo.cs
file.AssembyInfo.cs
file usually contains metadata about the company, product, and copyright information.The following are lines from an AssemblyInfo.cs
file:
[assembly: AssemblyTitle("SuperWidget")]
[assembly: AssemblyDescription("Implements the SuperWidget product.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("McArthur Widgets, Inc.")]
[assembly: AssemblyProduct("Super Widget Deluxe")]
[assembly: AssemblyCopyright("Copyright © McArthur Widgets 2010")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
You've probably noticed that the syntax for applying an attribute is very different from anything you've seen so far. From that, you might get the impression that attributes are an entirely different type of construct. They're not—they're just a special kind of class.
Some important points about attribute classes are the following:
System.Attribute
.Declaring an attribute class is, for the most part, the same as declaring any other class. There are, however, several things to be aware of:
System.Attribute
.Attribute
.sealed
.For example, the following code shows the beginning of the declaration of attribute MyAttributeAttribute
:
Since an attribute holds information about the target, the public members of an attribute class generally consist only of the following:
Attributes, like other classes, have constructors. Every attribute must have at least one public constructor.
For example, with the following constructor, the compiler would produce an error message if the name did not include the suffix:
When you apply an attribute to a target, you are specifying which constructor should be used to create the instance of the attribute. The parameters listed in the attribute application are the actual parameters for the constructor.
For example, in the following code, MyAttribute
is applied to a field and to a method. For the field, the declaration specifies a constructor with a single string
parameter. For the method, it specifies a constructor with two string
parameters.
[MyAttribute("Holds a value")] // Constructor with one string
public int MyField;
[MyAttribute("Version 1.3", "Sal Martin")] // Constructor with two strings
public void MyMethod()
{ ...
Other important points about attribute constructors are the following:
MyAttr
. The meanings of the two forms are the same.[MyAttr]
class SomeClass ...
[MyAttr()]
class OtherClass ...
You cannot call the constructor explicitly. An instance of an attribute is created, and a constructor called, only when an attribute consumer accesses the attribute. This is very different from other class instances, which are created at the position where you use an object-creation expression. Applying an attribute is a declarative statement that does not determine when an object of the attribute class should be constructed.
Figure 24-4 compares the use of a constructor for a regular class and the use of a constructor with attributes.
Figure 24-4. Comparing the use of constructors
Like the methods and constructors of regular classes, the attribute constructors can also use positional and named parameters.
The following code shows the application of an attribute using a positional parameter and two named parameters:
The following code shows the declaration of the attribute class, as well as its application on class MyClass
. Notice that the constructor declaration lists only a single formal parameter. And yet, by using named parameters, you can give the constructor three actual parameters. The two named parameters set the values of fields Ver
and Reviewer
.
Note If the constructor requires any positional parameters, they must be placed before any named parameters.
You've seen that you can apply attributes to classes. But attributes themselves are classes, and there is one important predefined attribute that you can apply to your custom attributes. It's the AttributeUsage
attribute. You can use it to restrict the usage of an attribute to a specific set of target types.
For example, if you want your custom attribute MyAttribute
to be applied only to methods, you could use the following form of AttributeUsage
:
AttributeUsage
has three important public properties, which are listed in Table 24-4. The table shows the names of the properties and their meanings. For the second two properties, it also shows their default values.
Table 24-4. Public Properties of AttributeUsage
Name | Meaning | Default |
ValidOn |
Stores a list of the types of targets to which the attribute can be applied. The first parameter of the constructor must be an enum value of type AttributeTarget . |
|
Inherited |
A Boolean value that specifies whether the attribute can be inherited by derived classes of the decorated type. | true |
AllowMultiple |
A Boolean value that specifies whether the target can have multiple instances of the attribute applied to it. | false |
The constructor for AttributeUsage
takes a single, positional parameter that specifies which target types are allowed for the attribute. It uses this parameter to set the ValidOn
property. The acceptable target types are members of the AttributeTarget
enumeration. Table 24-5 shows the complete set of the members of the AttributeTarget
enumeration.
You can combine the usage types by using the bitwise OR operator. For example, the attribute declared in the following code can be applied only to methods and constructors.
Table 24-5. Members of Enum AttributeTarget
All |
Assembly |
Class |
Constructor |
Delegate |
Enum |
Event |
Field |
GenericParameter |
Interface |
Method |
Module |
Parameter |
Property |
ReturnValue |
Struct |
When you apply AttributeUsage
to an attribute declaration, the constructor will have at least the one required parameter, which contains the target types to be stored in ValidOn
. You can also optionally set the Inherited
and AllowMultiple
properties by using named parameters. If you don't set them, they'll have their default values, as shown in Table 24-4.
As an example, the next code block specifies the following about MyAttribute
:
MyAttribute
must be applied only to classes.MyAttribute
is not inherited by classes derived from classes to which it is applied.MyAttribute
applied to the same target.[ AttributeUsage( AttributeTarget.Class, // Required, positional
Inherited = false, // Optional, named
AllowMultiple = false ) ] // Optional, named
public sealed class MyAttributeAttribute : System.Attribute
{ ...
The following practices are strongly suggested when writing custom attributes:
sealed
.AttributeUsage
attribute on your attribute declaration to explicitly specify the set of attribute targets.The following code illustrates these guidelines:
[AttributeUsage( AttributeTargets.Class )]
public sealed class ReviewCommentAttribute : System.Attribute
{
public string Description { get; set; }
public string VersionNumber { get; set; }
public string ReviewerID { get; set; }
public ReviewCommentAttribute(string desc, string ver)
{
Description = desc;
VersionNumber = ver;
}
}
At the beginning of the chapter, you saw that you can access information about a type using its Type
object. You can access custom attributes in the same way. There are two methods of Type
that are particularly useful in this: IsDefined
and GetCustomAttributes
.
You can use the IsDefined
method of the Type
object to determine whether a particular attribute is applied to a particular class.
For example, the following code declares an attributed class called MyClass
and also acts as its own attribute consumer by accessing an attribute declared and applied in the program itself. At the top of the code are declarations of the attribute ReviewComment
and the class MyClass
, to which it is applied. The code does the following:
Main
creates an object of the class. It then retrieves a reference to the Type
object by using the GetType
method, which it inherited from its base class, object
.Type
object, it can call the IsDefined
method to find out whether attribute ReviewComment
is applied to this class.
Type
object of the attribute you are checking for.bool
and specifies whether to search the inheritance tree of MyClass
to find the attribute.[AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute : System.Attribute
{ ... }
[ReviewComment("Check it out", "2.4")]
class MyClass { }
class Program {
static void Main() {
MyClass mc = new MyClass(); // Create an instance of the class.
Type t = mc.GetType(); // Get the Type object from the instance.
bool isDefined = // Check the Type for the attribute.
t.IsDefined(typeof(ReviewCommentAttribute), false);
if( isDefined )
Console.WriteLine("ReviewComment is applied to type {0}", t.Name);
}
}
This code produces the following output:
ReviewComment is applied to type MyClass
The GetCustomAttributes
method returns an array of the attributes applied to a construct.
object
s, which you must then cast to the correct attribute type.object[] AttArr = t.GetCustomAttributes(false);
GetCustomAttributes
method is called, an instance of each attribute associated with the target is created.The following code uses the same attribute and class declarations as the previous example. But in this case, it doesn't just determine whether an attribute is applied to the class. Instead, it retrieves an array of the attributes applied to the class and cycles through them, printing out their member values.
static void Main()
{
Type t = typeof(MyClass);
object[] AttArr = t.GetCustomAttributes(false);
foreach (Attribute a in AttArr)
{
ReviewCommentAttribute attr = a as ReviewCommentAttribute;
if (null != attr)
{
Console.WriteLine("Description : {0}", attr.Description);
Console.WriteLine("Version Number : {0}", attr.VersionNumber);
Console.WriteLine("Reviewer ID : {0}", attr.ReviewerID);
}
}
}
This code produces the following output:
Description : Check it out
Version Number : 2.4
Reviewer ID :
18.188.119.81