Custom Attributes

 
Chapter 5 - C# and the Base Classes
bySimon Robinsonet al.
Wrox Press 2002
  

We saw in Chapter 4 how it is possible to define attributes on various items within your program. The attributes that we encountered in that chapter were all ones that Microsoft has defined, however, and which the C# compiler has specific knowledge of. 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 and related attributes).

The architecture of attributes also allows you to define your own attributes in your sourcecode. Clearly, if you do this, these attributes will not have any effect on the compilation process itself, because the compiler has no intrinsic awareness of them, but these attributes will be emitted as metadata in the compiled assembly. By itself, this metadata may be useful for documentation purposes. However, what makes this idea really powerful is that using the classes in the System.Reflection namespace, your code can read this metadata at runtime. This means that the custom attributes that you define can have a direct effect on how your code runs.

We are going to examine the process of defining and using the custom attributes here, and then we will see how to use these in conjunction with reflection in the next section of the chapter. Over these two sections of the chapter, we will develop an example based on a company that regularly ships upgrades to its software, and wishes to have details of these upgrades documented automatically. In the example, we will define custom attributes that indicate the date that classes or methods in code were last modified or created, and what changes were made. We will then use reflection to develop an application that looks for these attributes in an assembly, and hence can automatically display all the details about what upgrades have been made to the software since a given date. This kind of application can be very useful in saving you work on documentation writing and ensuring your customers get all the up-to-date information whenever you ship a new version of the software package.

Another example of how you might use attributes and reflection would be if your application reads from or writes to a database; then you could use custom attributes as a way of marking which classes and properties correspond to which database tables and columns . Then, by reading these attributes in from the assembly at runtime, your program would be able to automatically retrieve or write data to the appropriate location in the database, without having to code up any specific logic for each table or column.

Writing Custom Attributes

In order to understand how to write custom attributes, it is useful to see what the compiler actually does when it encounters an item in your code that has been marked with an attribute, for which implicit support is not built into the compiler. To take our database example, suppose you have a C# property declaration that looks like this:

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

On seeing that this property has an attribute, FieldName , the C# compiler will start off by appending the string Attribute to this name, forming the combined name FieldNameAttribute , and will then search all the namespaces in its search path (that is, those namespaces that have been mentioned in a using statement) for a class that has the same name. Note, however, that if you mark an item with an attribute whose name already ends in the string Attribute , then the compiler won't bother adding that string to the name, but will leave the string that indicates the attribute name unchanged. The above code is exactly equivalent to this:

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

The compiler will expect to find a class with this name and it will expect this class to be derived from System.Attribute . The compiler is also expecting that this class will contain information that governs the usage of this attribute in certain well-defined ways. In particular, the attribute class needs to specify which items in a program it can be applied to (classes, structs, properties, methods, and so on), and whether it is legal for it to be applied more than once to the same item, as well as what compulsory and optional parameters this 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 (for example, if the attribute class indicates that the attribute could only be applied to fields, but you have applied it in your sourcecode to a struct definition), then the compiler will raise a compilation error. Therefore, the next step is to make sure we have defined an appropriate custom attribute class.

Custom Attribute Classes

Continuing the above example, let's assume we have defined a 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;     }     }   

What we have here is just enough information to let the compiler know how to use the attribute.

AttributeUsage Attribute

The first thing to note is that our attribute class itself is marked with an attribute the AttributeUsage attribute. This is another one of those attributes that the C# compiler intrinsically knows what to do with (you could argue that AttributeUsage isn't an attribute at all; it is more like a meta-attribute, because it applies to other attributes, not simply to any class). AttributeUsage is there primarily to indicate to which items in your code your custom attribute can be applied. This information is given by its first parameter, which must be present. This parameter is of an enumerated type, AttributeTargets . In the above example, we have indicated that the FieldName attribute may be applied only to properties, which is fine, because that is exactly what we have applied it to in our earlier code fragment. The definition of the AttributeTargets enumeration is:

   public enum AttributeTargets     {     All = 0x00003FFF,     Assembly = 0x00000001,     Class = 0x00000004,     Constructor = 0x00000020,     Delegate = 0x00001000,     Enum = 0x00000010,     Event = 0x00000200,     Field = 0x00000100,     Interface = 0x00000400,     Method = 0x00000040,     Module = 0x00000002,     Parameter = 0x00000800,     Property = 0x00000080,     ReturnValue = 0x00002000,     Struct = 0x00000008     }   

This list tells us all of the elements that attributes may be applied to. Note that when applying the attribute to a program element, we place the attribute in square brackets immediately before the element. However, there is one value in the above list that does not correspond to any program element: Assembly . An attribute can be applied to an assembly as a whole instead of to an element in your code; in this case the attribute can be placed anywhere in your sourcecode, but needs to be marked with the assembly keyword:

   [assembly: SomeAssemblyAttribute(Parameters)]   

When indicating the elements, it is quite possible to combine these values together using the bitwise OR operator. For example, if we wanted to indicate that our FieldName attribute could be applied to either a property or a field, we could have written:

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

You can also use AttributeTargets.All to indicate that your attribute is allowed effectively anywhere. The AttributeUsage attribute as illustrated above also contains two other parameters, AllowMultiple and Inherited . These are indicated with a different syntax of < AttributeName > = < AttributeValue >, instead of simply giving the values for these attributes in order. These parameters are optional parameters you can omit them if you wish.

The AllowMultiple parameter indicates whether an attribute may 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 , then this indicates that an attribute that is applied to a class or interface will also automatically be applied to all inherited classes or interfaces. If the attribute is applied to a method or property, and so on, then it will automatically apply to any overrides of that method or property, and so on.

Specifying Attribute Parameters

Now let's examine how we can specify any parameters that our 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 to the attribute that will take exactly those parameters. If it finds one, that's OK. If it doesn't it will raise a compilation error. It's as if the compiler wants to instantiate an attribute object, although that's not actually what happens. The compiler simply emits metadata into the assembly. However, as we will see soon, an attribute object may later be instantiated if a later program uses reflection to examine the attributes in the assembly, so the compiler needs to make sure that the emitted metadata is of the appropriate types to allow this to happen.

In our case, we have supplied just one constructor for FieldNameAttribute , and this constructor takes one string parameter. Therefore, when applying the FieldName attribute to a property, we must supply one string as a parameter, as we have done in the code just presented.

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

Optional Parameters

We have seen in the AttributeUsage attribute that there is an alternative syntax by which optional parameters can be added to an attribute. This syntax involves specifying the names of the optional parameters. It works through properties or fields in the attribute class. For example, suppose we modified our 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 will recognize the < ParameterName > = syntax of the second parameter, and so not attempt to match this parameter to a FieldNameAttribute constructor. Instead, it will look for a public property (or field, but as indicated in previous chapters, 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 we want the above code to work, we had better add some code to FieldNameAttribute :

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

With this code added and the implementation of the Comment property filled in, we can now supply optional attributes.

The WhatsNewAttributes Example

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

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

  • The VectorStruct assembly, which contains the code to which the attributes have been applied. For this one, we have just taken the Vector sample that we have developed through the last few chapters.

  • 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 we have used up until now. The remaining two assemblies are simply libraries they each contain class definitions, but no program entry point. For the VectorStruct assembly, this means that we have taken the VectorAsCollection sample and removed the entry point and test harness class, leaving only the Vector class itself.

Managing three related projects by compiling at the command line is fiddly, so although we will present the commands for separately compiling all these source files, if you download this code sample from the Wrox Press web site, you may prefer to edit it as a combined Visual Studio .NET solution, in the way that we will demonstrate in Chapter 6. The files are alternatively available as Visual Studio .NET solutions to allow you to do this.

The WhatsNewAttributes Library Assembly

We will start off with the core WhatsNewAttributes project itself. The sourcecode is contained in the file WhatsNewAttributes.cs . We have not compiled to libraries before, but the syntax for doing this is quite simple. At the command line we supply the flag target:library to the compiler. To compile WhatsNewAttributes , type in:

  csc /target:library WhatsNewAttributes.cs  

The sourcecode for this assembly contains two attribute classes, LastModifiedAttribute and SupportsWhatsNewAttribute . LastModifiedAttribute is the attribute that we can use to mark when an item was last modified. It takes two compulsory parameters (the parameters that are passed to the constructor); the date of the modifications, and a string containing a description of the changes. There is also one optional parameter (the parameter for which a writeable property exists), issues , which can be used to describe any outstanding issues for the item.

There is no difference in the sourcecode between code intended as a library and code intended as an application, except there is no Main() method in a library.

In real life you would probably want this attribute to apply to anything. In order to keep our code simple, we are going to limit its usage here to classes and methods. We will allow it to be applied more than once to the same item however, ( AllowMultiple=true ) since an item may get modified more than once, and each modification will need 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 we are maintaining documentation via the LastModifiedAttribute . This is so that the program that will examine this assembly later on knows that the assembly it is reading is one that we are actually using our automated documentation process on! Here is the complete sourcecode for this part of the example:

   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     {     }     }   

From the previous descriptions, the above code should all be clear. Notice, however, that we have not bothered to supply set accessors to the Changes and DateModified properties. There is no need, since we are requiring these parameters to be set in the constructor as compulsory parameters (you may wonder what we need the get accessors for; that's so that when we need to read the values of these attributes later on, we will be able to do so).

Using these Attributes The VectorClass Assembly

Next, we need to use these attributes. For this, as mentioned before, we are using a modified version of the earlier VectorAsCollection sample. Note that we need to explicitly reference the WhatsNewAttributes library that we have just created. We also need to indicate the corresponding namespace with a using statement if the compiler is to be able to recognize the attributes:

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

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

Now for the code for the Vector class. We are not really changing anything in this class, just adding a couple of LastModified attributes to mark out the work that we have done on this class in this chapter. We have made one change, however: we have defined Vector as a class instead of a struct. The only reason for this is to simplify the code that we will later write that displays the attributes. In the VectorAsCollection sample, Vector was a struct, but its enumerator was a class. This would have meant that our later sample that looks at this assembly would have had to pick out both classes and structs. Having both types as classes means we don't have to worry about the existence of any structs, thus making our example a bit shorter:

 namespace Wrox.ProCSharp.VectorClass {   [LastModified("14 Feb 2002", "IEnumerable interface implemented\n" +     "So Vector can now be treated as a collection")]     [LastModified("10 Feb 2002", "IFormattable interface implemented\n" +     "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(); 

We will 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     {   

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

In order to compile this code from the command line you should type the following:

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


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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