As well as containing code and data, a .NET program can also contain metadata. Metadata is information about the data—that is, information about the types, code, fields, and so on—stored along with your program. This chapter explores how some of that metadata is created and used.
A lot of the metadata is information that .NET needs in order to
understand how your code should be used—for example, metadata defines
whether a particular method is public
or
private
. But you can also add custom
metadata, using attributes.
Reflection is the process by which a program can read its own metadata, or metadata from another program. A program is said to reflect on itself or on another program, extracting metadata from the reflected assembly and using that metadata either to inform the user or to modify the program’s behavior.
An attribute is an object that
represents data you want to associate with an element in your program. The
element to which you attach an attribute is referred to as the target of that attribute. For example,
in Chapter 12 we saw the XmlIgnore
attribute applied to a
property:
[XmlIgnore] public string LastName { get; set; }
This tells the XML serialization system that we want it to ignore this
particular property when converting between XML and objects of this kind.
This illustrates an important feature of attributes: they don’t do
anything on their own. The XmlIgnore
attribute contains no code, nor does it cause anything to happen when the
relevant property is read or modified. It only has any effect when we use
XML serialization, and the only reason it does anything then is because
the XML serialization system goes looking for it.
Attributes are passive. They are essentially just annotations. For them to be useful, something somewhere needs to look for them.
Some attributes are supplied as part of the CLR, some by the . NET Framework class libraries, and some by other libraries. In addition, you are free to define custom attributes for your own purposes.
Most programmers will use only the attributes provided by existing libraries, though creating your own custom attributes can be a powerful tool when combined with reflection, as described later in this chapter.
If you search through the .NET Framework class libraries, you’ll
find a great many attributes. Some attributes can be applied to an
assembly, others to a class or interface, and some, such as [XmlIgnore]
, are applied to properties and
fields. Most attributes make sense only when applied to certain
things—the XmlIgnore
attribute
cannot usefully be applied to a method, for example, because methods
cannot be serialized to XML. So each attribute type declares its
attribute targets using the AttributeTargets
enumeration. Most of the
entries in this enumeration are self-explanatory, but since a few are
not entirely obvious, Table 17-1
shows a complete list.
Table 17-1. Possible attribute targets
Member name | Attribute may be applied to |
---|---|
| Any of the following elements: assembly, class, constructor, delegate, enum, event, field, interface, method, module, parameter, property, return value, or struct |
| An assembly |
| A class |
| A constructor |
| A delegate |
| An enumeration |
| An event |
| A field |
| A type parameter for a generic class or method |
| An interface |
| A method |
| A module |
| A parameter of a method |
| A property (both
|
| A return value |
| A struct |
You apply most attributes to their targets by placing them in square brackets immediately before the target item. A couple of the target types don’t correspond directly to any single source code feature, and so these are handled differently. For example, an assembly is a single compiled .NET executable or library—it’s everything in a single project—so there’s no one feature in the source code to which to apply the attribute. Therefore, you can apply assembly attributes at the top of any file. The module attribute target type works the same way.[48]
You must place assembly or module attributes after all
using
directives and before any
code.
You can apply multiple attributes, one after another:
[assembly: AssemblyDelaySign(false)] [assembly: AssemblyKeyFile(".\\keyFile.snk")]
Alternatively, you can put them all inside a single pair of square brackets, separating the attributes with commas:
[assembly: AssemblyDelaySign(false), assembly: AssemblyKeyFile(".\\keyFile.snk")]
The System.Reflection
namespace offers a number of attributes, including attributes for
assemblies (such as the AssemblyKeyFileAttribute
), for
configuration, and for version attributes. Some of these are
recognized by the compiler—the key file attribute gets used if the
compiler generates a digital signature for your component, for
example.
You are free to create your own custom attributes and use them at runtime as you see fit. Suppose, for example, that your development organization wants to keep track of bug fixes. You already keep a database of all your bugs, but you’d like to tie your bug reports to specific fixes in the code.
You might add comments to your code along the lines of:
// Bug 323 fixed by Jesse Liberty 1/1/2010.
This would make it easy to see in your source code, but since comments get stripped out at compile time this information won’t make it into the compiled code. If we wanted to change that, we could use a custom attribute. We would replace the comment with something like this:
[BugFixAttribute(323, "Jesse Liberty", "1/1/2010", Comment="Off by one error")]
You could then write a program to read through the metadata to find these bug-fix annotations, and perhaps it might go on to update a bug database. The attribute would serve the purposes of a comment, but would also allow you to retrieve the information programmatically through tools you’d create.
This may be a somewhat artificial example, however, because you might not really want this information to be compiled into the shipping code.
Attributes, like most things in C#, are embodied in classes. To
create a custom attribute, derive a class from System.Attribute
:
public class BugFixAttribute : System.Attribute
You need to tell the compiler which kinds of elements this attribute can be used with (the attribute target). We specify this with (what else?) an attribute:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)]
AttributeUsage
is an
attribute applied to an attribute class. It provides data about the
metadata: a meta-attribute, if you will.
We have provided the AttributeUsage
attribute constructor with
two arguments. The first is a set of flags that indicate the target—in
this case, the class and its constructor, fields, methods, and
properties. The second argument is a flag that indicates whether a
given element might receive more than one such attribute. In this
example, AllowMultiple
is set to true
, indicating that class members can have
more than one BugFixAttribute
assigned.
The new custom attribute in this example is named BugFixAttribute
. The convention is to append
the word Attribute to your attribute name. The
compiler recognizes this convention, by allowing you to use a shorter
version of the name when you apply the attribute. Thus, you can
write:
[BugFix(123, "Jesse Liberty", "01/01/08", Comment="Off by one")]
The compiler will first look for an attribute class named
BugFix
and, if it doesn’t find
that, will then look for BugFixAttribute
.
Although attributes have constructors, the syntax we use when
applying an attribute is not quite the same as that for a normal
constructor. We can provide two types of argument:
positional and named. In the
BugFix
example, the programmer’s
name, the bug ID, and the date are positional parameters, and comment
is a named parameter. Positional
parameters are passed in through the constructor, and must be passed
in the order declared in the constructor:
public BugFixAttribute(int bugID, string programmer, string date) { this.BugID = bugID; this.Programmer = programmer; this.Date = date; }
Named parameters are implemented as fields or as properties:
public string Comment { get; set; }
You may be wondering why attributes use a different syntax for
named arguments than we use in normal method and constructor
invocation, where named arguments take the form Comment: "Off by one"
, using a colon
instead of an equals sign. The inconsistency is for historical
reasons. Attributes have always supported positional and named
arguments, but methods and normal constructor calls only got them in
C# 4.0. The mechanisms work quite differently—the C# 4.0 named
argument syntax is mainly there to support optional arguments, and
it only deals with real method arguments, whereas with attributes,
named arguments are not arguments at all—they are really properties
in disguise.
It is common to create read-only properties for the positional parameters:
public int BugID { get; private set; }
Once you have defined an attribute, you can put it to work by
placing it immediately before its target. To test the BugFixAttribute
of the preceding example,
the following program creates a simple class named MyMath
and gives it two functions. Assign
BugFixAttribute
s to the class to record
its code-maintenance history:
[BugFixAttribute(121,"Jesse Liberty","01/03/08")] [BugFixAttribute(107,"Jesse Liberty","01/04/08", Comment="Fixed off by one errors")] public class MyMath
These attributes are stored with the metadata. Example 17-1 shows the complete program.
Example 17-1. Working with custom attributes
using System; namespace CustomAttributes { // create custom attribute to be assigned to class members [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] public class BugFixAttribute : System.Attribute { // attribute constructor for positional parameters public BugFixAttribute ( int bugID, string programmer, string date ) { this.BugID = bugID; this.Programmer = programmer; this.Date = date; } // accessors public int BugID { get; private set; } public string Date { get; private set; } public string Programmer { get; private set; } // property for named parameter public string Comment { get; set; } } // ********* assign the attributes to the class ******** [BugFixAttribute(121, "Jesse Liberty", "01/03/08")] [BugFixAttribute(107, "Jesse Liberty", "01/04/08", Comment = "Fixed off by one errors")] public class MyMath { public double DoFunc1(double param1) { return param1 + DoFunc2(param1); } public double DoFunc2(double param1) { return param1 / 3; } } public class Tester { static void Main(string[] args) { MyMath mm = new MyMath(); Console.WriteLine("Calling DoFunc(7). Result: {0}", mm.DoFunc1(7)); } } } Output: Calling DoFunc(7). Result: 9.3333333333333333
As you can see, the attributes had absolutely no impact on the output. This is not surprising because, as we said earlier, attributes are passive—they only affect things that go looking for them, and we’ve not yet written anything that does that. In fact, for the moment, you have only our word that the attributes exist at all. We’ll see how to get at this metadata and use it in a program in the next section.
[48] Modules are the individual files that constitute an assembly. The vast majority of assemblies consist of just one file, so it’s very rare to encounter situations in which you need to deal with an individual module as opposed to the whole assembly. They are mentioned here for completeness.