Reflection, Attributes, and Dynamic Programming
Attributes are a means of inserting additional metadata into an assembly and associating the metadata with a programming construct such as a class, method, or property. This chapter investigates the details surrounding attributes that are built into the framework and describes how to define custom attributes. To take advantage of custom attributes, it is necessary to identify them. This is handled through reflection. This chapter begins with a look at reflection, including how you can use it to dynamically bind at execution time based on member invocation by name (or metadata) at compile time. Reflection is frequently leveraged within tools such as a code generator. In addition, reflection is used at execution time when the call target is unknown.
The chapter ends with a discussion of dynamic programming1, a feature that greatly simplifies working with data that is dynamic and requires execution-time rather than compile-time binding.
Using reflection, it is possible to do the following.
Reflection is the process of examining the metadata within an assembly. Traditionally, when code compiles down to a machine language, all the metadata (such as type and method names) about the code is discarded. In contrast, when C# compiles into the Common Intermediate Language (CIL), it maintains most of the metadata about the code. Furthermore, using reflection, it is possible to enumerate through all the types within an assembly and search for those that match certain criteria. You access a type’s metadata through instances of System.Type, and this object includes methods for enumerating the type instance’s members. Additionally, it is possible to invoke those members on objects that are of the examined type.
The facility for reflection enables a host of new paradigms that otherwise are unavailable. For example, reflection enables you to enumerate over all the types within an assembly, along with their members, and in the process create stubs for documentation of the assembly API. To create the API documentation, you can then combine the metadata retrieved from reflection with the XML document created from XML comments (using the /doc switch). Similarly, programmers can use reflection metadata to generate code for persisting (serializing) business objects into a database. It can also be used in a list control that displays a collection of objects. Given the collection, a list control could use reflection to iterate over all the properties of an object in the collection, defining a column within the list for each property. Furthermore, by invoking each property on each object, the list control could populate each row and column with the data contained in the object, even though the data type of the object is unknown at compile time.
XmlSerializer, ValueType, and the Microsoft .NET Framework’s DataBinder are a few of the classes in the framework that use reflection for portions of their implementation as well.
The key to reading a type’s metadata is to obtain an instance of System.Type that represents the target type instance. System.Type provides all the methods for retrieving the information about a type. You can use it to answer questions such as the following:
There are more such members, but all of them provide information about a particular type. The key is to obtain a reference to a type’s Type object, and the two primary ways to do so are through object.GetType() and typeof().
Note that the GetMethods() call does not return extension methods. These methods are available only as static members on the implementing type.
object includes a GetType() member, so all types necessarily include this function. You call GetType() to retrieve an instance of System.Type corresponding to the original object. Listing 18.1 demonstrates this process, using a Type instance from DateTime. Output 18.1 shows the results.
After calling GetType(), you iterate over each System.Reflection.PropertyInfo instance returned from Type.GetProperties() and display the property names. The key to calling GetType() is that you must have an object instance. However, sometimes no such instance is available. Static classes, for example, cannot be instantiated, so there is no way to call GetType() with them.
Another way to retrieve a Type object is with the typeof expression. typeof binds at compile time to a particular Type instance, and it takes a type directly as a parameter. The exception is for the type parameter on a generic type, as it isn’t determined until runtime. Listing 18.2 demonstrates the use of typeof with Enum.Parse().
In this listing, Enum.Parse() takes a Type object identifying an enum and then converts a string to the specific enum value. In this case, it converts "Idle" to System.Diagnostics.ThreadPriorityLevel.Idle.
Similarly, Listing 18.3 in the next section uses the typeof expression inside the CompareTo(object obj) method to verify that the type of the obj parameter was indeed what was expected:
if(obj.GetType() != typeof(Contact)) { ... }
The typeof expression is resolved at compile time such that a type comparison—perhaps comparing the type returned from a call to GetType()—can determine if an object is of a specific type.
The possibilities with reflection don’t stop with retrieving the metadata. You can also take the metadata and dynamically invoke the members it identifies. Consider the possibility of defining a class to represent an application’s command line.2 The difficulty with a CommandLineInfo class such as this relates to populating the class with the actual command-line data that started the application. However, using reflection, you can map the command-line options to property names and then dynamically set the properties at runtime. Listing 18.3 demonstrates this process.
Although Listing 18.3 is long, the code is relatively simple. Main() begins by instantiating a CommandLineInfo class. This type is defined specifically to contain the command-line data for this program. Each property corresponds to a command-line option for the program, where the command line is as shown in Output 18.2.
The CommandLineInfo object is passed to the CommandLineHandler’s TryParse() method. This method begins by enumerating through each option and separating out the option name (e.g., Help or Out). Once the name is determined, the code reflects on the CommandLineInfo object, looking for an instance property with the same name. If it finds such a property, it assigns the property using a call to SetValue() and specifies the data corresponding to the property type. (For arguments, this call accepts the object on which to set the value, the new value, and an additional index parameter that is null unless the property is an indexer.) This listing handles three property types: Boolean, string, and enum. In the case of enums, you parse the option value and assign the text’s enum equivalent to the property. Assuming the TryParse() call was successful, the method exits and the CommandLineInfo object is initialized with the data from the command line.
Interestingly, although CommandLineInfo is a private class nested within Program, CommandLineHandler has no trouble reflecting over it and even invoking its members. In other words, reflection can circumvent accessibility rules as long as appropriate permissions are established. For example, if Out was private, the TryParse() method could still assign it a value. Because of this, it would be possible to move CommandLineHandler into a separate assembly and share it across multiple programs, each with its own CommandLineInfo class.
In this example, you invoke a member on CommandLineInfo using PropertyInfo.SetValue(). Not surprisingly, PropertyInfo also includes a GetValue() method for retrieving data from the property. For a method, however, there is a MethodInfo class with an Invoke() member. Both MethodInfo and PropertyInfo derive from MemberInfo (albeit indirectly), as shown in Figure 18.1.