Custom Attributes


You've seen how you can define attributes on various items within your program. These attributes have been defined by Microsoft as part of the .NET Framework class library, many of which receive special support by the C# compiler. This means that for those particular attributes, the compiler could customize the compilation process in specific ways; for example, laying out a struct in memory according to the details in the StructLayout attributes.

The .Net Framework also allows you to define your own attributes. Clearly, these attributes will not have any effect on the compilation process, because the compiler has no intrinsic awareness of them. However, these attributes will be emitted as metadata in the compiled assembly when they are applied to program elements.

By itself, this metadata might be useful for documentation purposes, but what makes attributes really powerful is that using reflection, your code can read this metadata and use it to make decisions at runtime. This means that the custom attributes that you define can have a direct effect on how your code runs. For example, custom attributes can be used to enable declarative code access security checks for custom permission classes, associate information with program elements that can then be used by testing tools, or when developing extensible frameworks that allow the loading of plugins or modules.

Writing Custom Attributes

To understand how to write custom attributes, it is useful to know what the compiler does when it encounters an element in your code that has a custom attribute applied to it. To take the database example, suppose you have a C# property declaration that looks like this:

 [FieldName("SocialSecurityNumber")] public string SocialSecurityNumber { get { // etc. 

When the C# compiler recognizes that this property has an attribute applied to it (FieldName), it will start by appending the string Attribute to this name, forming the combined name FieldNameAttribute. The compiler will then search all the namespaces in its search path (those namespaces that have been mentioned in a using statement) for a class with the specified name. Note that if you mark an item with an attribute whose name already ends in the string Attribute, the compiler won't add the string to the name a second time, leaving the attribute name unchanged. Therefore, the preceding code is equivalent to this:

 [FieldNameAttribute("SocialSecurityNumber")] public string SocialSecurityNumber { get { // etc. 

The compiler expects to find a class with this name, and it expects this class to be derived directly or indirectly from System.Attribute. The compiler also expects that this class contains information that governs the use of the attribute. In particular, the attribute class needs to specify the following:

  • The types of program elements to which the attribute can be applied (classes, structs, properties, methods, and so on).

  • Whether it is legal for the attribute to be applied more than once to the same program element.

  • Whether the attribute, when applied to a class or interface, is inherited by derived classes and interfaces.

  • The mandatory and optional parameters the attribute takes.

If the compiler cannot find a corresponding attribute class, or it finds one but the way that you have used that attribute doesn't match the information in the attribute class, the compiler will raise a compilation error. For example, if the attribute class indicates that the attribute can only be applied to classes, but you have applied it to a struct definition, a compilation error will occur.

To continue with the example, assume you have defined the FieldName attribute like this:

 [AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=false)] public class FieldNameAttribute : Attribute { private string name; public FieldNameAttribute(string name) { this.name = name; } } 

The following sections discuss each element of this definition.

AttributeUsage attribute

The first thing to note is that the attribute class itself is marked with an attribute — the System. AttributeUsage attribute. This is an attribute defined by Microsoft for which the C# compiler provides special support. (You could argue that AttributeUsage isn't an attribute at all; it is more like a meta- attribute, because it applies only to other attributes, not simply to any class.) The primary purpose of AttributeUsage is to identify the types of program elements to which your custom attribute can be applied. This information is given by the first parameter of the AttributeUsage attribute — this parameter is mandatory, and is of an enumerated type, AttributeTargets. In the previous example, you have indicated that the FieldName attribute can be applied only to properties, which is fine, because that is exactly what you have applied it to in the earlier code fragment. The members of the AttributeTargets enumeration are

  • All

  • Assembly

  • Class

  • Constructor

  • Delegate

  • Enum

  • Event

  • Field

  • GenericParameter (.NET 2.0 only)

  • Interface

  • Method

  • Module

  • Parameter

  • Property

  • ReturnValue

  • Struct

This list identifies all of the program elements to which you can apply attributes. Note that when applying the attribute to a program element, you place the attribute in square brackets immediately before the element. However, two values in the preceding list do not correspond to any program element: Assembly and Module. An attribute can be applied to an assembly or module as a whole instead of to an element in your code; in this case the attribute can be placed anywhere in your source code, but needs to be prefixed with the Assembly or Module keyword:

 [assembly:SomeAssemblyAttribute(Parameters)]  [module:SomeAssemblyAttribute(Parameters)] 

When indicating the valid target elements of a custom attribute, you can combine these values using the bitwise OR operator. For example, if you wanted to indicate that your FieldName attribute can be applied to both properties and fields, you would write:

 [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field,   AllowMultiple=false,   Inherited=false)] public class FieldNameAttribute : Attribute 

You can also use AttributeTargets.All to indicate that your attribute can be applied to all types of program elements. The AttributeUsage attribute also contains two other parameters, AllowMultiple and Inherited. These are specified using the syntax of <ParameterName>=<ParameterValue>, instead of simply giving the values for these parameters. These parameters are optional — you can omit them if you want.

The AllowMultiple parameter indicates whether an attribute can be applied more than once to the same item. The fact that it is set to false here indicates that the compiler should raise an error if it sees something like this:

 [FieldName("SocialSecurityNumber")] [FieldName("NationalInsuranceNumber")] public string SocialSecurityNumber { // etc. 

If the Inherited parameter is set to true, an attribute applied to a class or interface will also automatically be applied to all derived classes or interfaces. If the attribute is applied to a method or property, it will automatically apply to any overrides of that method or property and so on.

Specifying attribute parameters

This section examines how you can specify the parameters that your custom attribute takes. The way it works is that when the compiler encounters a statement such as

 [FieldName("SocialSecurityNumber")] public string SocialSecurityNumber { // etc. 

it examines the parameters passed into the attribute — in this case a string — and looks for a constructor for the attribute that takes exactly those parameters. If the compiler finds an appropriate constructor, the compiler will emit the specified metadata to the assembly. If the compiler doesn't find an appropriate constructor, a compilation error occurs. As discussed later in this chapter, reflection involves reading metadata (attributes) from assemblies and instantiating the attribute classes they represent. Because of this, the compiler must ensure an appropriate constructor exists that will allow the runtime instantiation of the specified attribute.

In the example, you have supplied just one constructor for FieldNameAttribute, and this constructor takes one string parameter. Therefore, when applying the FieldName attribute to a property, you must supply one string as a parameter, as you have done in the preceding sample code.

If you want to allow a choice of what types of parameters should be supplied with an attribute, you can provide different constructor overloads, although normal practice is to supply just one constructor, and use properties to define any other optional parameters, as explained next.

Specifying optional attribute parameters

As demonstrated with reference to the AttributeUsage attribute, an alternative syntax exists by which optional parameters can be added to an attribute. This syntax involves specifying the names and values of the optional parameters. It works through public properties or fields in the attribute class. For example, suppose you modified the definition of the SocialSecurityNumber property as follows:

  [FieldName("SocialSecurityNumber", Comment="This is the primary key field")] public string SocialSecurityNumber {    // etc.

In this case, the compiler recognizes the <ParameterName>= <ParameterValue> syntax of the second parameter and does not attempt to match this parameter to a FieldNameAttribute constructor. Instead, it looks for a public property or field (although public fields are not considered good programming practice, so normally you will work with properties) of that name that it can use to set the value of this parameter. If you want the previous code to work, you have to add some code to FieldNameAttribute:

[AttributeUsage(AttributeTargets.Property,    AllowMultiple=false,    Inherited=false)] public class FieldNameAttribute : Attribute { private string comment; public string Comment { get { return comment; } set { comment = value; } } // etc. 

Custom Attribute Example: WhatsNewAttributes

In this section, you start developing the WhatsNewAttributes example described earlier, which provides for an attribute that indicates when a program element was last modified. This is a rather more ambitious code sample than many of the others in that it consists of three separate assemblies:

  • The WhatsNewAttributes assembly, which contains the definitions of the attributes

  • The VectorClass assembly, which contains the code to which the attributes have been applied

  • The LookUpWhatsNew assembly, which contains the project that displays details of items that have changed

Of these, only LookUpWhatsNew is a console application of the type that you have used up until now. The remaining two assemblies are libraries — they each contain class definitions, but no program entry point. For the VectorClass assembly, this means that the entry point and test harness class have been removed from the VectorAsCollection sample, leaving only the Vector class.

Managing three related assemblies by compiling at the command line is tricky; and although the commands for compiling all these source files are provided separately, you might prefer to edit the code sample (which you can download from the Wrox Web site at www.wrox.com) as a combined Visual Studio .NET solution as discussed in Chapter 14, "Visual Studio 2005." The download includes the required Visual Studio 2005 solution files.

The WhatsNewAttributes library assembly

This section starts off with the core WhatsNewAttributes assembly. The source code is contained in the file WhatsNewAttributes.cs, which is in the WhatsNewAttributes project of the WhatsNewAttributes solution in the example code for this chapter. The syntax for doing this is quite simple. At the command line you supply the flag target:library to the compiler. To compile WhatsNewAttributes, type:

 csc /target:library WhatsNewAttributes.cs 

The WhatsNewAttributes.cs file defines two attribute classes, LastModifiedAttribute and SupportsWhatsNewAttribute. LastModifiedAttribute is the attribute that you can use to mark when an item was last modified. It takes two mandatory parameters (parameters that are passed to the constructor): the date of the modification and a string containing a description of the changes. There is also one optional parameter named issues (for which a public property exists), which can be used to describe any outstanding issues for the item.

In real life you would probably want this attribute to apply to anything. To keep the code simple, its usage is limited here to classes and methods. You will allow it to be applied more than once to the same item (AllowMultiple=true) because an item might be modified more than once, and each modification will have to be marked with a separate attribute instance.

SupportsWhatsNew is a smaller class representing an attribute that doesn't take any parameters. The idea of this attribute is that it's an assembly attribute that is used to mark an assembly for which you are maintaining documentation via the LastModifiedAttribute. This way, the program that will examine this assembly later on knows that the assembly it is reading is one on which you are actually using your automated documentation process. Here is the complete source code for this part of the example:

 using System; namespace Wrox.ProCSharp.WhatsNewAttributes { [AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=true, Inherited=false)] public class LastModifiedAttribute : Attribute { private DateTime dateModified; private string changes; private string issues; public LastModifiedAttribute(string dateModified, string changes) { this.dateModified = DateTime.Parse(dateModified); this.changes = changes; } public DateTime DateModified { get { return dateModified; } } public string Changes { get { return changes; } } public string Issues { get { return issues; } set { issues = value; } } } [AttributeUsage(AttributeTargets.Assembly)] public class SupportsWhatsNewAttribute : Attribute { } } 

This code should be clear with reference to previous descriptions. Notice, however, that we have not bothered to supply set accessors to the Changes and DateModified properties. There is no need for these accessors, because you are requiring these parameters to be set in the constructor as mandatory parameters. You need the get accessors so that you can read the values of these attributes.

The VectorClass assembly

Next, you need to use these attributes. To this end, you use a modified version of the earlier VectorAs Collection sample. Note that you need to reference the WhatsNewAttributes library that you have just created. You also need to indicate the corresponding namespace with a using statement so the compiler can recognize the attributes:

using System; using Wrox.ProCSharp.WhatsNewAttributes; using System.Collections; using System.Text; [assembly: SupportsWhatsNew] 

In this code, you have also added the line that will mark the assembly itself with the SupportsWhatsNew attribute.

Now for the code for the Vector class. You are not making any major changes to this class; you only add a couple of LastModified attributes to mark out the work that you have done on this class in this chapter, and Vector is defined as a class instead of a struct to simplify the code (of the next iteration of the sample) that displays the attributes. (In the VectorAsCollection sample, Vector is a struct, but its enumerator is a class. This means the next iteration of the sample would have had to pick out both classes and structs when looking at the assembly, which would have made the example less straightforward.)

namespace Wrox.ProCSharp.VectorClass { [LastModified("14 Feb 2002", "IEnumerable interface implemented " + "So Vector can now be treated as a collection")] [LastModified("10 Feb 2002", "IFormattable interface implemented " + "So Vector now responds to format specifiers N and VE")] class Vector : IFormattable, IEnumerable {       public double x, y, z;       public Vector(double x, double y, double z)       {          this.x = x;          this.y = y;          this.z = z;       } [LastModified("10 Feb 2002", "Method added in order to provide formatting support")] public string ToString(string format, IFormatProvider formatProvider)       {          if (format == null)             return ToString();

You also mark the contained VectorEnumerator class as new:

 [LastModified("14 Feb 2002", "Class created as part of collection support for Vector")] private class VectorEnumerator : IEnumerator { 

To compile this code from the command line, type the following:

 csc /target:library /reference:WhatsNewAttributes.dll VectorClass.cs 

That's as far as you can get with this example for now. You can't run anything yet, because all you have are two libraries. You develop the final part of the example, in which you look up and display these attributes, as soon as you've had a look at how reflection works.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net