Working with Attributes

Working with Attributes

Like delegates and events, attributes provide a method for adding code to classes; unlike those approaches, however, the functionality is contributed in a more opaque manner. The source code behind the functionality exposed by attributes is usually hidden from view. Instead, attributes are used in a declarative way to decorate assemblies, classes, and other source code elements.

Attributes are classes, and all attributes are derived from the System.Attribute class. The .NET Framework includes many attributes that are used when developing applications for the .NET platform. For example, the Obsolete attribute is used to mark source code elements that are no longer favored, like this:

[Obsolete("Use the new BubbleSorter class")]
class BubbleSort
{
    
    

}

If you use a class, method, or other element marked with the Obsolete attribute, the compiler will issue a warning (or optionally, an error). During compilation, the compiler inspects all types used by your program, and if it detects the Obsolete attribute in a type or method used by your code, it issues a warning (or an error).

Later in this section, you’ll learn how to define custom attributes as well as how to write code to detect those attributes after compilation. But first we’ll take a detailed look at the C# syntax used with attributes.

Using Attributes

Attributes are placed within a set of square brackets, immediately before their associated programming element. The preceding example illustrated the Obsolete attribute attached to a class declaration. This attribute also can be associated with methods, as follows:

[Obsolete()]
public void Hide()
{
    
    

The syntax for an attribute is similar to that for a constructor, with the parameters passed as arguments within parentheses. Parameters passed as shown in the following code are known as positional parameters because they’re identified by their position in the list of parameters:

[Obsolete("This class is obsolete", true)]

An attribute can also include named parameters, in which properties and public fields are passed by name rather than position, as follows:

[WebService(Namespace="http://www.microsoft.com")]

Named parameters are always passed after any positional parameters; a named parameter passed before a positional parameter is flagged by the compiler as an error.

Resolving Attributes

When an attribute is attached to a class or other programming element, no executable code is generated as a result of that association. Instead, metadata is added to the assembly that identifies the attributes, as shown in Figure 6-3.

Figure 6-3.
Compilation of attributes into assembly metadata.

The attribute is instantiated and potentially used only when the attribute is examined by a class that is expressly searching for the attribute. In the case of the Obsolete attribute, the compiler performs the search. For other attributes, such as the attributes that control connection pooling and transactioning, the attributes are detected by classes in the .NET Framework at runtime.

For example, as you’ll see in Chapter 19, any types that you create can potentially be serialized into an XML document. (Serializing a type involves generating a stream or file from an instance of the type.) The default behavior of the XML serializer is sufficient to properly serialize objects to and from XML for most types. The .NET Framework also includes a number of attributes that provide fine-grained control over the XML serialization process. To prevent a field or property from being serialized, the XmlIgnore attribute is used, like this:

[XmlIgnore()]
public int Size;

In addition to specifying code elements that are to be ignored, attributes can be used to specify namespaces and alternative element names to the XML serializer. You don’t need to write code to manage the serialization process; you simply adorn your existing code with attributes that control how the serializer works.

As the serializer examines objects that are passed to it, it uses a .NET feature known as reflection to search the metadata for relevant serialization attributes. (Reflection is discussed in more detail later in this chapter, in the section “Determining Whether an Attribute Is Used.”) When an attribute is found, the XML serializer creates an instance of the attribute and uses the attribute object to control the serialization process. At least three classes are involved in the interaction with even the simplest attribute, as follows:

  • The class that defines the attribute

  • The class or other type that uses the attribute

  • The class that detects that attribute and performs work based on the attribute’s existence

As you’ll see in the next section, most of the work performed for an attribute takes place in the code that detects the presence of the attribute. The actual attribute class is used only to collect the properties that are stored by the attribute.

Defining Custom Attributes

In addition to the wide variety of attributes that are defined as part of the .NET Framework, you can define your own custom attributes derived from the System.Attribute class. By convention, all attribute classes end with Attribute, so the Obsolete attribute used earlier is actually named ObsoleteAttribute. When the C# compiler encounters an attribute, it will look first for the name specified within the square brackets—in this case, Obsolete. If that type name isn’t found, or if that type name isn’t derived from the System.Attribute class, the compiler will automatically append Attribute to the name and search again.

Declaring an AuthorAttribute Class

The AuthorAttribute class shown here provides a means of tracking the author of various program elements in your source code:

public class AuthorAttribute: Attribute
{
    public AuthorAttribute(string name)
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }

    public string Notes
    {
        set { _notes = value; }
        get { return _notes; }
    }

    protected string _name;
    protected string _notes;
}

The AuthorAttribute class includes the following two fields:

  • _name  Contains the name of the author of a particular class, method, or other code element that’s adorned with the attribute. The value of this member is set in the constructor and is accessible through the Name property.

  • _notes  Contains optional notes that are associated with a code element. This member is accessible only through the Notes property.

The AuthorAttribute class contains no code that detects the usage of its attribute or that takes action based on the presence of the Author attribute. We’ll write that code shortly; first let’s look at how the Author attribute is used.

Using the Author Attribute

To use the Author attribute defined in the preceding section, just add the attribute before a program element such as a class, passing the class author’s name as a parameter to the attribute, as shown here:

[Author("Mickey Williams")]
public class Sailboat
{
    
    

}

The Author attribute also can be used with a field, property, or other class member, as shown here:

[Author("Mickey Williams")]
public string Name
{
    
    

}

[Author("Mickey Williams")]
protected string _name; 

An interesting issue arises when you tag an assembly with an attribute. Unlike program elements that exist in your source code, an assembly is compiled, so there’s no clear location for an assembly attribute. For this reason, Microsoft Visual C# .NET automatically creates the AssemblyInfo.cs source file, which contains assembly attributes. Each assembly attribute uses the following format:

[assembly:attribute()]

The first part of the assembly annotation, assembly, is an attribute target. The attribute identifier notifies the compiler that the attribute is intended for a specific program element. To use the Author attribute with an assembly, the proper syntax is as follows:

[assembly: Author("Mickey Williams")]

The ability to specify a target for an attribute is useful in other situations in which the compiler can’t determine the intended target for an attribute. For example, attributes can be declared for return values or methods. Given the following attribute usage, the C# compiler will assume that the attribute is intended for the method declaration:

[SerializationName("BOOL")]
public bool AvoidWhale()
{
    
    

}

If your intent was to annotate the method return value, however, you must explicitly provide an attribute target.

There are nine attribute types, as follows:

  • assembly

  • event

  • field

  • method

  • module

  • param

  • property

  • return

  • type

For example, to specify that an attribute targets the return value rather than the method, the syntax would look like this:

[return:SerializationName("BOOL")]
public bool AvoidWhale()
{
    
    

}
Determining Whether an Attribute Is Used

As mentioned, attributes add no executable code to your assembly. Instead, information about an attribute is added to the assembly’s metadata, and you must write code that examines the metadata to determine whether an attribute is being used. We’ll use classes from the System.Reflection namespace to query metadata and retrieve information about custom attributes. Reflection is a powerful technique that allows you to determine, at runtime, information about types that are available to you.

A common starting point when using the reflection classes is to retrieve a System.Type object that describes the type of object you’re interested in. In a C# program, all objects expose a GetType method that returns an instance of the System.Type class, as follows:

CSharpAuthor me = new CSharpAuthor();
Type myType = me.GetType();

Alternatively, if you don’t have an instance of the type, you can use the typeof operator with the type name, like this:

Type myType = typeof(CSharpAuthor);

Armed with a System.Type object, you can use the reflection classes to retrieve the custom attributes that annotate the type, as shown in the following code:

public class AuthorAttributeCheck
{
    public AuthorAttributeCheck(Type theType)
    {
        _type = theType;
    }

    public string GetAuthorName()
    {
        foreach(Attribute attrib in _type.GetCustomAttributes(true))
        {
            AuthorAttribute auth = attrib as AuthorAttribute;
            if(auth != null)
            {
                return auth.Name;
            }
        }
        return null;
    }

    public string GetNotes()
    {
        foreach(Attribute attrib in _type.GetCustomAttributes(true))
        {
            AuthorAttribute auth = attrib as AuthorAttribute;
            if(auth != null)
            {
                return auth.Notes;
            }
        }
        return null;
    }
    protected Type _type;
}

The AuthorAttributeCheck class accepts a System.Type object as a parameter and then caches the type as a member field. The methods used to retrieve attribute information work in similar ways: first the array of custom attributes is retrieved by calling _type.GetCustomAttributes, and then each attribute is tested to determine whether it’s an instance of AuthorAttribute. If the attribute has the proper type, properties are retrieved from the attribute and returned to the caller, as shown below.

foreach(Attribute attrib in _type.GetCustomAttributes(true))
{
    AuthorAttribute auth = attrib as AuthorAttribute;
    if(auth != null)
    {
        // Use auth attribute here.
        
    

    }
}

The AuthorAttributeCheck class determines whether the Author attribute is used, retrieving information if the attribute is present. To use the Author­Attribute class, first pass the type to be checked as a constructor parameter, and then call methods to retrieve the author name and notes properties, as shown here:

Type t = typeof(Sailboat);
AuthorAttributeCheck check = new AuthorAttributeCheck(t);
string name = check.GetAuthorName();
if(name != null)
    Console.WriteLine("The author's name is: {0}", name);

The preceding version of AuthorAttributeCheck detects when an Author attribute is attached to a type, but it doesn’t detect cases in which the Author attribute is attached to a field, method, or other type member. It’s reasonable to attach an Author attribute to a field, because several authors might have been involved in creating the code for a class. An attribute attached to a field is detected using slightly different reflection code, as shown here:

public string[] GetAllMemberAuthorInfo()
{
    MemberInfo[] members = _type.GetMembers();
    string [] result = new string[members.Length];
    int n = 0;
    foreach(MemberInfo info in members)
    {
        string name = info.Name;
        string memberType = info.MemberType.ToString();
        string author = MemberToAuthor(info);
        if(author == null)
            author = "Not known";
        result[n] = memberType + ": " + name + ", author:" + author;
        ++n;
    }
    return result;
}

protected string MemberToAuthor(MemberInfo info)
{
    foreach(Attribute attrib in info.GetCustomAttributes(true))
    {
        AuthorAttribute auth = attrib as AuthorAttribute;
        if(auth != null)
        {
            return auth.Name;
        }
    }
    return null;
}

The methods described here are part of the final version of AuthorAttributeCheck. They make it possible to determine whether any members of a type are adorned with the Author attribute. GetAllMemberAuthorInfo calls the _type.GetMembers method to retrieve the array of members defined for the type. This array contains constructors, methods, fields, properties, and other program elements that are declared for a particular type. The custom attributes for each member are then checked for the presence of the Author attribute.

It’s also possible to use reflection to examine a specific member, instead of retrieving an array containing information for all members. The GetMember method returns an array of MemberInfo for a specific member name that’s passed as a parameter. The GetMemberAuthorInfo method shown here uses GetMember to return author information for a specific member:

public string[] GetMemberAuthorInfo(string memberName)
{
    MemberInfo[] members = _type.GetMember(memberName);
    string [] result = new string[members.Length];
    int n = 0;
    foreach(MemberInfo info in members)
    {
        string name = info.Name;
        string memberType = info.MemberType.ToString();
        string author = MemberToAuthor(info);
        if(author == null)
            author = "Not known";
        result[n] = memberType + ": " + name + ", author:" + author;
        ++n;
    }
    return result;
}

Reflection can also be used to retrieve custom attribute information for assemblies, as shown in the following GetAssemblyAuthorName method:

public string GetAssemblyAuthorName(string asmName)
{
    Assembly asm = Assembly.Load(asmName);
    if(asm != null)
    {
        foreach(Attribute attrib in asm.GetCustomAttributes(true))
        {
            AuthorAttribute auth = attrib as AuthorAttribute;
            if(auth != null)
            {
                return auth.Name;
            }
        }
    }
    return null;
}

This GetAssemblyAuthorName method starts by retrieving an instance of the Assembly class that refers to an assembly passed as a parameter. As with the previous examples, the array of custom attributes is retrieved, and if an AuthorAttribute instance is detected, the Name property is returned to the caller.

Controlling Custom Attribute Usage

As a practical matter, you’ll often want to control the usage of your custom attributes. The usage restrictions for custom elements are specified by using the AttributeUsage attribute, as follows:

[AttributeUsage(AttributeTargets.All)]
public class AuthorAttribute: Attribute
{
    
    

}

The AttributeUsage attribute has the following three parameters:

  • ValidOn  Positional parameter that defines the program elements that are eligible to use the attribute

  • AllowMultiple  Named parameter that specifies whether an attribute can be used multiple times in the same program element

  • Inherited  Named parameter that indicates whether the attribute should be inherited by subclasses of the type

The ValidOn parameter’s value must use one of the values from the AttributeTargets enumeration, listed here:

  • All

  • Assembly

  • Class

  • Constructor

  • Delegate

  • Enum

  • Event

  • Field

  • Interface

  • Method

  • Module

  • Parameter

  • Property

  • ReturnValue

  • Struct

Values can be combined using the OR (¦) operator. For example, to allow an attribute to be used on classes or structs, combine two values from the enumeration like this:

[AttributeUsage(AttributeTargets.Class¦AttributeTargets.Struct)]

Custom attributes aren’t inherited in subclasses by default. If you want your attribute to be inherited when its target is subclassed or overridden, set the Inherited parameter to true, as follows:

[AttributeUsage(AttributeTargets.All, Inherited=true)]

By default, attributes can be attached to a target only once. To allow your custom attribute to be used multiple times on the same attribute target, set the AllowMultiple parameter to true, as follows:

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

In most cases, it makes sense to limit an attribute to a single use per target. However, consider the case in which an attribute is used to trace bug fixes. Because a class or method can have multiple bug fixes in its lifetime, you should allow the attribute to be used multiple times, as shown here:

[Bug(id="20011004", fixedby="mw")]
[Bug(id="20011001", fixedby="mw")]
class ScoopBinder
{
    
    

}
Creating a Strong Name

A common use of attributes is to provide a strong name for an assembly. As discussed in Chapter 1, assemblies that have a strong name can be placed in the global assembly cache. The global assembly cache enables a single assembly to be shared among multiple projects. The global assembly cache can be used to share multiple copies of the same assembly, as long as the copies differ in their version number or culture. The global assembly cache can store multiple copies of an assembly because it uses an assembly’s unique strong name, instead of its file name, as an identifier. There are four components to a strong name, as follows:

  • The file system’s name for the assembly

  • The assembly’s version number

  • A cryptographic key pair used to sign the assembly

  • An optional culture designation that’s used for localized assemblies

Creating a Cryptographic Key Pair

As part of the strong-naming process, the assembly is digitally signed using a cryptographic key pair. There are two portions to the cryptographic key: a public portion, which is exposed to everyone, and a private portion, which remains secret. A hash of the assembly’s contents is combined with the private portion of the key pair; the hash value is written into the assembly, along with the public portion of the key pair. A hash of the public portion of the key, commonly known as the public key token, is written into any assemblies that reference an assembly with a strong name.

The runtime calculates the proper hash value when loading the assembly and guarantees that the assembly contents have not been tampered with by a third party. Additionally, when a new version of a strong-named assembly is stored in the global assembly cache, the runtime ensures that the same key pair was used to sign the new version of the assembly.

So how do you create a cryptographic key pair? Although a cryptographic service provider (CSP) can be used as a source for your key pair, there’s no need to use one. The simplest way to obtain a key pair is to run the Strong Name utility (sn.exe), which is part of the .NET Framework. The following command generates a key pair and stores it in a file named MyKeys.snk. (You must execute this command from the Microsoft Visual Studio .NET command prompt.)

sn –k MyKeys.snk

Although you can use the Assembly Linker (al.exe) to sign an assembly with a key pair, the simplest technique is to reference the key pair file using an attribute in your source code, enabling Visual C# to sign the assembly when it is compiled. The key pair is referenced using the AssemblyKeyFile attribute, which is located in the AssemblyInfo.cs source file included in every Visual C# project. By default, the AssemblyKeyFile attribute is provided a default empty value, as shown here:

[assembly: AssemblyKeyFile("")]

To sign your assembly, pass the key pair’s file path to the AssemblyKeyFile attribute. The path is relative to the assembly binary, rather than to the project. If the file containing the key pair is located in the project directory, you must adjust the path accordingly, like this:

[assembly: AssemblyKeyFile(@"..\..\MyKeyPair.snk")]
Deploying Assemblies into the Global Assembly Cache

The preferred method for deploying an assembly into the global assembly cache is to use Microsoft Windows Installer 2.0 or later, which is fully aware of the global assembly cache and will properly install assemblies into it. For development and debugging purposes, you can use the Global Assembly Cache Utility tool (gacutil.exe). To register an assembly, use a command such as this from the Visual Studio .NET command prompt:

gacutil /i myctrl.dll

To remove an assembly from the global assembly cache, use the following command:

gacutil /u myctrl.dll

This command removes all copies of myctrl.dll. To remove a specific version, pass the version and public key token, as shown here:

gacutil /u myctrl.dll,Version=1.2.3.4,PublicKeyToken=8e091308dafe6804

The Global Assembly Cache Utility tool isn’t meant to be distributed to end users. As you’ve seen, it’s easy to remove items from the global assembly cache, and the tool is meant for use only on your development and test computers.



Part III: Programming Windows Forms