Chapter 17. Attributes and Reflection

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.

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.

Note

This may be a somewhat artificial example, however, because you might not really want this information to be compiled into the shipping code.

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

Note

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 BugFixAttributes 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.

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.