From this book, you have 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, and many of them receive special support from the 0# 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 Struct Layout 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 wiIl 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 by 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 directly affect how your code runs. For example, custom attributes can be used to enable declarative code access security checks for custom permission classes, to associate information with program elements that can then be used by testing tools, or when developing extensible framework that allow the loading of plugins or modules.
Writing Custom Attributes
To understand how to write your own 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 that you have a C# property declaration that looks like this:
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 wiIl 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 will not add the string to the name a second time; it will leave the attribute name unchanged. Therefore, the preceding code is equivalent to this:
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 does not 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 be applied only to classes, but you have applied it to a struct definition, a compilation error will occur.
To continue with the example, assume that you have defined the FieldName attribute like this:
The following sections discuss each element of this definition.
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 CI#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 At tributeUsage attribute – this parameter is mandatory, and is of an enumerated type, AttributeTargetl. 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:
- Generic Parameter
- Return Value
Custom Attribute Example: WhatsNewAttributes
In this section, you start developing the example mentioned at the beginning of the chapter. WhatsNewAttributes provides for an attribute that indicates when a program element was last modified. This is a more ambitious code sample than many of the others in that it consists o! 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 Lookup WhatsNew 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. These classes are represented later in this chapter.
Managing three related assemblies by compiling at the command line is tricky. 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 Csharp Web site at www.csharpaid.com) as a combined Visual Studio solution.
The WhatsNewAttributes Library Assembly
This section starts with the core WhatsNewAttributes assembly. The source code is contained in the file WhatsNewAttributes. cs, which is located 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 the following:
csc /target:library WhatsNewAttributes.cs
The WhatsNewAttributes. cs file defines two attribute classes, LaatModifiedAttribute and SupportsWhatsNewAttribute. The attribute, LastModifiedAttribute, is the attribute that you can use to mark when an item was last modified. It takes two mandatory patameters (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 practice, 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 is 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:
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.