Chapter 15

Reflection

WHAT’S IN THIS CHAPTER?

  • Using custom attributes
  • Inspecting the metadata at runtime using reflection
  • Building access points from classes that enable reflection

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for this chapter are found at http://www.wrox.com/remtitle.cgi?isbn=1118314425 on the Download Code tab. The code for this chapter is divided into the following major examples:

  • LookupWhatsNew
  • TypeView
  • VectorClass
  • WhatsNewAttributes

MANIPULATING AND INSPECTING CODE AT RUNTIME

This chapter focuses on custom attributes and reflection. Custom attributes are mechanisms that enable you to associate custom metadata with program elements. This metadata is created at compile time and embedded in an assembly. Reflection is a generic term that describes the capability to inspect and manipulate program elements at runtime. For example, reflection allows you to do the following:

  • Enumerate the members of a type
  • Instantiate a new object
  • Execute the members of an object
  • Find out information about a type
  • Find out information about an assembly
  • Inspect the custom attributes applied to a type
  • Create and compile a new assembly

This list represents a great deal of functionality and encompasses some of the most powerful and complex capabilities provided by the .NET Framework class library. Because one chapter does not have the space to cover all the capabilities of reflection, it focuses on those elements that you are likely to use most frequently.

To demonstrate custom attributes and reflection, in this chapter you first develop an example based on a company that regularly ships upgrades of its software and wants to have details about these upgrades documented automatically. In the example, you define custom attributes that indicate the date when program elements were last modified, and what changes were made. You then use reflection to develop an application that looks for these attributes in an assembly and can automatically display all the details about what upgrades have been made to the software since a given date.

Another example in this chapter considers an application that reads from or writes to a database and uses custom attributes as a way to mark which classes and properties correspond to which database tables and columns. By reading these attributes from the assembly at runtime, the program can automatically retrieve or write data to the appropriate location in the database, without requiring specific logic for each table or column.

CUSTOM ATTRIBUTES

You have already seen in this book 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 C# compiler. This means that for those particular attributes, the compiler can customize the compilation process in specific ways — for example, laying out a struct in memory according to the details in the StructLayout attributes.

The .NET Framework also enables you to define your own attributes. Obviously, these attributes won’t have any effect on the compilation process because the compiler has no intrinsic awareness of them. However, these attributes will 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 frameworks that allow the loading of plug-ins 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:

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

When the C# compiler recognizes that this property has an attribute applied to it (FieldName), it first appends the string Attribute to this name, forming the combined name FieldNameAttribute. The compiler then searches 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:

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

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 governing 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 if 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.

Continuing with the example, assume that you have defined the 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;
   }
}

The following sections discuss each element of this definition.

AttributeUsage Attribute

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 C# 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 provided by the first parameter of the AttributeUsage attribute. This parameter is mandatory, and it is of an enumerated type, AttributeTargets. 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 as follows:

  • All
  • Assembly
  • Class
  • Constructor
  • Delegate
  • Enum
  • Event
  • Field
  • GenericParameter (.NET 2.0 and higher only)
  • Interface
  • Method
  • Module
  • Parameter
  • Property
  • ReturnValue
  • Struct

This list identifies all the program elements to which you can apply attributes. Note that when applying the attribute to a program element, you place the attribute in square brackets immediately before the element. However, two values in the preceding list do not correspond to any program element: Assembly and Module. An attribute can be applied to an assembly or a module as a whole, rather than to an element in your code; in this case the attribute can be placed anywhere in your source code, but it must be prefixed with the Assembly or Module keyword:

[assembly:SomeAssemblyAttribute(Parameters)]
[module:SomeAssemblyAttribute(Parameters)]

When indicating the valid target elements of a custom attribute, you can combine these values using the bitwise OR operator. For example, if you want to indicate that your FieldName attribute can be applied to both properties and fields, you would use the following:

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

You can also use AttributeTargets.All to indicate that your attribute can be applied to all types of program elements. The AttributeUsage attribute also contains two other parameters, AllowMultiple and Inherited. These are specified using the syntax of <ParameterName>=<ParameterValue>, instead of simply specifying the values for these parameters. These parameters are optional — you can omit them.

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

Specifying Attribute Parameters

This section demonstrates how you can specify the parameters that your custom attribute takes. When the compiler encounters a statement such as the following, it examines the parameters passed into the attribute — which is a string — and looks for a constructor for the attribute that takes exactly those parameters:

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

If the compiler finds an appropriate constructor, it emits the specified metadata to the assembly. If the compiler does not find an appropriate constructor, a compilation error occurs. As discussed later in this chapter, reflection involves reading metadata (attributes) from assemblies and instantiating the attribute classes they represent. Because of this, the compiler must ensure that an appropriate constructor exists that will allow the runtime instantiation of the specified attribute.

In the example, you have supplied just one constructor for FieldNameAttribute, and this constructor takes one string parameter. Therefore, when applying the FieldName attribute to a property, you must supply one string as a parameter, as shown in the preceding code.

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

Specifying Optional Attribute Parameters

As demonstrated with the AttributeUsage attribute, an alternative syntax enables optional parameters to be added to an attribute. This syntax involves specifying the names and values of the optional parameters. It works through public properties or fields in the attribute class. For example, suppose that you modify the 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 recognizes the <ParameterName>=<ParameterValue> syntax of the second parameter and does not attempt to match this parameter to a FieldNameAttribute constructor. Instead, it looks for a public property or field (although 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 you want the previous code to work, you have to add some code to FieldNameAttribute:

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

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 example than many of the others in that it consists of three separate assemblies:

  • WhatsNewAttributes — Contains the definitions of the attributes
  • VectorClass — Contains the code to which the attributes have been applied
  • LookUpWhatsNew — Contains the project that displays details about items that have changed

Of these, only the LookUpWhatsNew assembly 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 Wrox web site at www.wrox.com) as a combined Visual Studio solution, as discussed in Chapter 17, “Visual Studio 2012.” The download includes the required Visual Studio 2012 solution files.

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 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, LastModifiedAttribute and SupportsWhatsNewAttribute. You use the attribute LastModifiedAttribute to mark when an item was last modified. It takes two mandatory parameters (parameters that are passed to the constructor): the date of the modification and a string containing a description of the changes. One optional parameter named issues (for which a public property exists) 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 has to be marked with a separate attribute instance.

SupportsWhatsNew is a smaller class representing an attribute that doesn’t take any parameters. The purpose of this assembly attribute is to mark an assembly for which you are maintaining documentation via the LastModifiedAttribute. This way, the program that examines this assembly later 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 (code file WhatsNewAttributes.cs):

using System;
        
namespace WhatsNewAttributes
{
   [AttributeUsage(
      AttributeTargets.Class | AttributeTargets.Method,
      AllowMultiple=true, Inherited=false)]
   public class LastModifiedAttribute: Attribute
   {
        private readonly DateTime _dateModified;
        private readonly string _changes;
        
        public LastModifiedAttribute(string dateModified, string changes)
        {
            dateModified = DateTime.Parse(dateModified);
            _changes = changes;
        }
        
        public DateTime DateModified
        {
            get { return _dateModified; }
        }
        
        public string Changes
        {
            get { return _changes; }
        }
        
        public string Issues { get; set; }
   }
        
   [AttributeUsage(AttributeTargets.Assembly)]
   public class SupportsWhatsNewAttribute: Attribute
   {
   }
}

Based on what has been discussed, this code should be fairly clear. 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.

The VectorClass Assembly

To use these attributes, you will be using a modified version of the earlier VectorAsCollection example. Note that you need to reference the WhatsNewAttributes library that you just created. You also need to indicate the corresponding namespace with a using statement so the compiler can recognize the attributes:

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

This code also adds the line that marks the assembly itself with the SupportsWhatsNew attribute.

Now for the code for the Vector class. You are not making any major changes to this class; you only add a couple of LastModified attributes to mark the work that you have done on this class in this chapter. Then Vector is defined as a class instead of a struct to simplify the code (of the next iteration of the example) that displays the attributes. (In the VectorAsCollection example, Vector is a struct, but its enumerator is a class. This means that the next iteration of the example would have had to pick out both classes and structs when looking at the assembly, which would have made the example less straightforward.)

namespace VectorClass
{
   [LastModified("14 Feb 2010", "IEnumerable interface implemented " +
       "So Vector can now be treated as a collection")]
   [LastModified("10 Feb 2010", "IFormattable interface implemented " +
       "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 2010",
                    "Method added in order to provide formatting support")]
      public string ToString(string format, IFormatProvider formatProvider)
      {
         if (format == null)
         {
            return ToString();
         }

You also mark the contained VectorEnumerator class as new:

   [LastModified("14 Feb 2010",
                 "Class created as part of collection support for Vector")]
   private class VectorEnumerator: IEnumerator
   {

To compile this code from the command line, type the following:

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

That’s as far as you can get with this example for now. You are unable to run anything yet because all you have are two libraries. After taking a look at reflection in the next section, you will develop the final part of the example, in which you look up and display these attributes.

USING REFLECTION

In this section, you take a closer look at the System.Type class, which enables you to access information concerning the definition of any data type. You’ll also look at the System.Reflection.Assembly class, which you can use to access information about an assembly or to load that assembly into your program. Finally, you will combine the code in this section with the code in the previous section to complete the WhatsNewAttributes example.

The System.Type Class

So far you have used the Type class only to hold the reference to a type as follows:

Type t = typeof(double);

Although previously referred to as a class, Type is an abstract base class. Whenever you instantiate a Type object, you are actually instantiating a class derived from Type. Type has one derived class corresponding to each actual data type, though in general the derived classes simply provide different overloads of the various Type methods and properties that return the correct data for the corresponding data type. They do not typically add new methods or properties. In general, there are three common ways to obtain a Type reference that refers to any given type.

  • You can use the C# typeof operator as shown in the preceding code. This operator takes the name of the type (not in quotation marks, however) as a parameter.
  • You can use the GetType method, which all classes inherit from System.Object:
double d = 10;
Type t = d.GetType();

GetType is called against a variable, rather than taking the name of a type. Note, however, that the Type object returned is still associated with only that data type. It does not contain any information that relates to that instance of the type. The GetType method can be useful if you have a reference to an object but you are not sure what class that object is actually an instance of.

  • You can call the static method of the Type class, GetType:
Type t = Type.GetType("System.Double");

Type is really the gateway to much of the reflection functionality. It implements a huge number of methods and properties — far too many to provide a comprehensive list here. However, the following subsections should give you a good idea of the kinds of things you can do with the Type class. Note that the available properties are all read-only; you use Type to find out about the data type — you cannot use it to make any modifications to the type!

Type Properties

You can divide the properties implemented by Type into three categories. First, a number of properties retrieve the strings containing various names associated with the class, as shown in the following table:

PROPERTY RETURNS
Name The name of the data type
FullName The fully qualified name of the data type (including the namespace name)
Namespace The name of the namespace in which the data type is defined

Second, it is possible to retrieve references to further type objects that represent related classes, as shown in the following table.

PROPERTY RETURNS TYPE REFERENCE CORRESPONDING TO
BaseType The immediate base type of this type
UnderlyingSystemType The type to which this type maps in the .NET runtime (recall that certain .NET base types actually map to specific predefined types recognized by IL)

A number of Boolean properties indicate whether this type is, for example, a class, an enum, and so on. These properties include IsAbstract, IsArray, IsClass, IsEnum, IsInterface, IsPointer, IsPrimitive (one of the predefined primitive data types), IsPublic, IsSealed, and IsValueType. The following example uses a primitive data type:

Type intType = typeof(int);
Console.WriteLine(intType.IsAbstract);     // writes false
Console.WriteLine(intType.IsClass);        // writes false
Console.WriteLine(intType.IsEnum);         // writes false
Console.WriteLine(intType.IsPrimitive);    // writes true
Console.WriteLine(intType.IsValueType);    // writes true

This example uses the Vector class:

Type vecType = typeof(Vector);
Console.WriteLine(vecType.IsAbstract);     // writes false
Console.WriteLine(vecType.IsClass);        // writes true
Console.WriteLine(vecType.IsEnum);         // writes false
Console.WriteLine(vecType.IsPrimitive);    // writes false
Console.WriteLine(vecType.IsValueType);    // writes false

Finally, you can also retrieve a reference to the assembly in which the type is defined. This is returned as a reference to an instance of the System.Reflection.Assembly class, which is examined shortly:

Type t = typeof (Vector);
Assembly contai6ningAssembly = new Assembly(t);

Methods

Most of the methods of System.Type are used to obtain details about the members of the corresponding data type — the constructors, properties, methods, events, and so on. Quite a large number of methods exist, but they all follow the same pattern. For example, two methods retrieve details about the methods of the data type: GetMethod and GetMethods. GetMethod() returns a reference to a System.Reflection.MethodInfo object, which contains details about a method. GetMethods returns an array of such references. As the names suggest, the difference is that GetMethods returns details about all the methods, whereas GetMethod returns details about just one method with a specified parameter list. Both methods have overloads that take an extra parameter, a BindingFlags enumerated value that indicates which members should be returned — for example, whether to return public members, instance members, static members, and so on.

For example, the simplest overload of GetMethods takes no parameters and returns details about all the public methods of the data type:

Type t = typeof(double);
MethodInfo[] methods = t.GetMethods();
foreach (MethodInfo nextMethod in methods)
{
   // etc.
       }

The member methods of Type that follow the same pattern are shown in the following table. Note that plural names return an array.

TYPE OF OBJECT RETURNED METHOD(S)
ConstructorInfo GetConstructor(), GetConstructors()
EventInfo GetEvent(), GetEvents()
FieldInfo GetField(), GetFields()
MemberInfo GetMember(), GetMembers(), GetDefaultMembers()
MethodInfo GetMethod(), GetMethods()
PropertyInfo GetProperty(), GetProperties()

The GetMember and GetMembers methods return details about any or all members of the data type, regardless of whether these members are constructors, properties, methods, and so on.

The TypeView Example

This section demonstrates some of the features of the Type class with a short example, TypeView, which you can use to list the members of a data type. The example demonstrates how to use TypeView for a double; however, you can swap this type with any other data type just by changing one line of the code in the example. TypeView displays far more information than can be displayed in a console window, so we’re going to take a break from our normal practice and display the output in a message box. Running TypeView for a double produces the results shown in Figure 15-1.

The message box displays the name, full name, and namespace of the data type as well as the name of the underlying type and the base type. Next, it simply iterates through all the public instance members of the data type, displaying for each member the declaring type, the type of member (method, field, and so on), and the name of the member. The declaring type is the name of the class that actually declares the type member (for example, System.Double if it is defined or overridden in System.Double, or the name of the relevant base type if the member is simply inherited from a base class).

TypeView does not display signatures of methods because you are retrieving details about all public instance members through MemberInfo objects, and information about parameters is not available through a MemberInfo object. To retrieve that information, you would need references to MethodInfo and other more specific objects, which means that you would need to obtain details about each type of member separately.

TypeView does display details about all public instance members; but for doubles, the only ones defined are fields and methods. For this example, you will compile TypeView as a console application — there is no problem with displaying a message box from a console application. However, because you are using a message box, you need to reference the base class assembly System.Windows.Forms.dll, which contains the classes in the System.Windows.Forms namespace in which the MessageBox class that you will need is defined. The code for TypeView is as follows. To begin, you need to add a few using statements:

using System;
using System.Reflection;
using System.Text;
using System.Windows.Forms;

You need System.Text because you will be using a StringBuilder object to build up the text to be displayed in the message box, and System.Windows.Forms for the message box itself. The entire code is in one class, MainClass, which has a couple of static methods and one static field, a StringBuilder instance called OutputText, which will be used to build the text to be displayed in the message box. The main method and class declaration look like this:

   class MainClass
   {
      static StringBuilder OutputText = new StringBuilder();
        
      static void Main()
      {
         // modify this line to retrieve details of any
         // other data type
         Type t = typeof(double);
        
         AnalyzeType(t);
         MessageBox.Show(OutputText.ToString(), "Analysis of type "
                                                 + t.Name);
         Console.ReadLine();
      }

The Main method implementation starts by declaring a Type object to represent your chosen data type. You then call a method, AnalyzeType, which extracts the information from the Type object and uses it to build the output text. Finally, you show the output in a message box. Using the MessageBox class is fairly intuitive. You just call its static Show method, passing it two strings, which will, respectively, be the text in the box and the caption. AnalyzeType is where the bulk of the work is done:

      static void AnalyzeType(Type t)
      {
         AddToOutput("Type Name: " + t.Name);
         AddToOutput("Full Name: " + t.FullName);
         AddToOutput("Namespace: " + t.Namespace);
        
         Type tBase = t.BaseType;
        
         if (tBase != null)
         {
            AddToOutput("Base Type:" + tBase.Name);
         }
        
         Type tUnderlyingSystem = t.UnderlyingSystemType;
        
         if (tUnderlyingSystem != null)
         {
            AddToOutput("UnderlyingSystem Type:" + tUnderlyingSystem.Name);
         }
        
         AddToOutput("
PUBLIC MEMBERS:");
         MemberInfo [] Members = t.GetMembers();
        
         foreach (MemberInfo NextMember in Members)
         {
            AddToOutput(NextMember.DeclaringType + " " +
            NextMember.MemberType + " " + NextMember.Name);
         }
      }

You implement the AnalyzeType method by calling various properties of the Type object to get the information you need concerning the type names, then call the GetMembers method to get an array of MemberInfo objects that you can use to display the details for each member. Note that you use a helper method, AddToOutput, to build the text to be displayed in the message box:

      static void AddToOutput(string Text)
      {
         OutputText.Append("
" + Text);
      }

Compile the TypeView assembly using this command:

csc /reference:System.Windows.Forms.dll Program.cs

The Assembly Class

The Assembly class is defined in the System.Reflection namespace and provides access to the metadata for a given assembly. It also contains methods that enable you to load and even execute an assembly — assuming that the assembly is an executable. As with the Type class, Assembly contains too many methods and properties to cover here, so this section is confined to covering those methods and properties that you need to get started and that you will use to complete the WhatsNewAttributes example.

Before you can do anything with an Assembly instance, you need to load the corresponding assembly into the running process. You can do this with either the static members Assembly.Load or Assembly.LoadFrom. The difference between these methods is that Load takes the name of the assembly, and the runtime searches in a variety of locations in an attempt to locate the assembly. These locations include the local directory and the global assembly cache. LoadFrom takes the full path name of an assembly and does not attempt to find the assembly in any other location:

   Assembly assembly1 = Assembly.Load("SomeAssembly");
   Assembly assembly2 = Assembly.LoadFrom
      (@"C:My ProjectsSoftwareSomeOtherAssembly");

A number of other overloads of both methods exist, which supply additional security information. After you have loaded an assembly, you can use various properties on it to find out, for example, its full name:

   string name = assembly1.FullName;

Getting Details About Types Defined in an Assembly

One nice feature of the Assembly class is that it enables you to obtain details about all the types that are defined in the corresponding assembly. You simply call the Assembly.GetTypes method, which returns an array of System.Type references containing details about all the types. You can then manipulate these Type references as explained in the previous section:

Type[] types = theAssembly.GetTypes();
        
foreach(Type definedType in types)
{
   DoSomethingWith(definedType);
}

Getting Details About Custom Attributes

The methods you use to find out which custom attributes are defined on an assembly or type depend on the type of object to which the attribute is attached. If you want to find out what custom attributes are attached to an assembly as a whole, you need to call a static method of the Attribute class, GetCustomAttributes, passing in a reference to the assembly:


NOTE This is actually quite significant. You may have wondered why, when you defined custom attributes, you had to go to all the trouble of actually writing classes for them, and why Microsoft didn’t come up with some simpler syntax. Well, the answer is here. The custom attributes genuinely exist as objects, and when an assembly is loaded you can read in these attribute objects, examine their properties, and call their methods.

Attribute[] definedAttributes =
             Attribute.GetCustomAttributes(assembly1);
             // assembly1 is an Assembly object

GetCustomAttributes, which is used to get assembly attributes, has a few overloads. If you call it without specifying any parameters other than an assembly reference, it simply returns all the custom attributes defined for that assembly. You can also call GetCustomAttributes by specifying a second parameter, which is a Type object that indicates the attribute class in which you are interested. In this case, GetCustomAttributes returns an array consisting of all the attributes present that are of the specified type.

Note that all attributes are retrieved as plain Attribute references. If you want to call any of the methods or properties you defined for your custom attributes, you need to cast these references explicitly to the relevant custom attribute classes. You can obtain details about custom attributes that are attached to a given data type by calling another overload of Assembly.GetCustomAttributes, this time passing a Type reference that describes the type for which you want to retrieve any attached attributes. To obtain attributes that are attached to methods, constructors, fields, and so on, however, you need to call a GetCustomAttributes method that is a member of one of the classes MethodInfo, ConstructorInfo, FieldInfo, and so on.

If you expect only a single attribute of a given type, you can call the GetCustomAttribute method instead, which returns a single Attribute object. You will use GetCustomAttribute in the WhatsNewAttributes example to find out whether the SupportsWhatsNew attribute is present in the assembly. To do this, you call GetCustomAttribute, passing in a reference to the WhatsNewAttributes assembly, and the type of the SupportsWhatsNewAttribute attribute. If this attribute is present, you get an Attribute instance. If no instances of it are defined in the assembly, you get null. If two or more instances are found, GetCustomAttribute throws a System.Reflection.AmbiguousMatchException. This is what that call would look like:

   Attribute supportsAttribute =
             Attribute.GetCustomAttributes(assembly1,
             typeof(SupportsWhatsNewAttribute));

Completing the WhatsNewAttributes Example

You now have enough information to complete the WhatsNewAttributes example by writing the source code for the final assembly in the sample, the LookUpWhatsNew assembly. This part of the application is a console application. However, it needs to reference the other assemblies of WhatsNewAttributes and VectorClass. Although this is going to be a command-line application, you will follow the previous TypeView example in that you actually display the results in a message box because there is a lot of text output — too much to show in a console window screenshot.

The file is called LookUpWhatsNew.cs, and the command to compile it is as follows:

csc /reference:WhatsNewAttributes.dll /reference:VectorClass.dll LookUpWhatsNew.cs

In the source code of this file, you first indicate the namespaces you want to infer. System.Text is there because you need to use a StringBuilder object again:

using System;
using System.Reflection;
using System.Windows.Forms;
using System.Text;
using WhatsNewAttributes;
        
namespace LookUpWhatsNew
{

The class that contains the main program entry point as well as the other methods is WhatsNewChecker. All the methods you define are in this class, which also has two static fields — outputText, which contains the text as you build it in preparation for writing it to the message box, and backDateTo, which stores the date you have selected. All modifications made since this date will be displayed. Normally, you would display a dialog inviting the user to pick this date, but we don’t want to get sidetracked into that kind of code. For this reason, backDateTo is hard-coded to a value of 1 Feb 2010. You can easily change this date when you download the code:

   internal class WhatsNewChecker
   {
      private static readonly StringBuilder outputText = new StringBuilder(1000);
      private static DateTime backDateTo = new DateTime(2010, 2, 1);
        
      static void Main()
      {
         Assembly theAssembly = Assembly.Load("VectorClass");
         Attribute supportsAttribute =
            Attribute.GetCustomAttribute(
               theAssembly, typeof(SupportsWhatsNewAttribute));
         string name = theAssembly.FullName;
        
         AddToMessage("Assembly: " + name);
        
         if (supportsAttribute == null)
         {
            AddToMessage(
                "This assembly does not support WhatsNew attributes");
            return;
         }
         else
         {
            AddToMessage("Defined Types:");
         }
        
         Type[] types = theAssembly.GetTypes();
        
         foreach(Type definedType in types)
            DisplayTypeInfo(definedType);
        
         MessageBox.Show(outputText.ToString(),
            "What's New since " + backDateTo.ToLongDateString());
         Console.ReadLine();
      }

The Main method first loads the VectorClass assembly, and then verifies that it is marked with the SupportsWhatsNew attribute. You know VectorClass has the SupportsWhatsNew attribute applied to it because you have only recently compiled it, but this is a check that would be worth making if users were given a choice of which assembly they wanted to check.

Assuming that all is well, you use the Assembly.GetTypes method to get an array of all the types defined in this assembly, and then loop through them. For each one, you call a method, DisplayTypeInfo, which adds the relevant text, including details regarding any instances of LastModifiedAttribute, to the outputText field. Finally, you show the message box with the complete text. The DisplayTypeInfo method looks like this:

      private static void DisplayTypeInfo(Type type)
      {
         // make sure we only pick out classes
         if (!(type.IsClass))
         {
            return;
         }
        
         AddToMessage("
class " + type.Name);
        
         Attribute [] attribs = Attribute.GetCustomAttributes(type);
        
         if (attribs.Length == 0)
         {
            AddToMessage("No changes to this class
");
         }
         else
         {
            foreach (Attribute attrib in attribs)
            {
               WriteAttributeInfo(attrib);
            }
         }
        
         MethodInfo [] methods = type.GetMethods();
         AddToMessage("CHANGES TO METHODS OF THIS CLASS:");
        
         foreach (MethodInfo nextMethod in methods)
         {
            object [] attribs2 =
               nextMethod.GetCustomAttributes(
                  typeof(LastModifiedAttribute), false);
        
            if (attribs2 != null)
            {
               AddToMessage(
                  nextMethod.ReturnType + " " + nextMethod.Name + "()");
               foreach (Attribute nextAttrib in attribs2)
               {
                  WriteAttributeInfo(nextAttrib);
               }
            }
         }
      }

Notice that the first thing you do in this method is check whether the Type reference you have been passed actually represents a class. Because, to keep things simple, you have specified that the LastModified attribute can be applied only to classes or member methods, you would be wasting time by doing any processing if the item is not a class (it could be a class, delegate, or enum).

Next, you use the Attribute.GetCustomAttributes method to determine whether this class has any LastModifiedAttribute instances attached to it. If so, you add their details to the output text, using a helper method, WriteAttributeInfo.

Finally, you use the Type.GetMethods method to iterate through all the member methods of this data type, and then do the same with each method as you did for the class — check whether it has any LastModifiedAttribute instances attached to it; if so, you display them using WriteAttributeInfo.

The next bit of code shows the WriteAttributeInfo method, which is responsible for determining what text to display for a given LastModifiedAttribute instance. Note that this method is passed an Attribute reference, so it needs to cast this to a LastModifiedAttribute reference first. After it has done that, it uses the properties that you originally defined for this attribute to retrieve its parameters. It confirms that the date of the attribute is sufficiently recent before actually adding it to the text for display:

      private static void WriteAttributeInfo(Attribute attrib)
      {
        
         LastModifiedAttribute lastModifiedAttrib =
            attrib as LastModifiedAttribute;
        
         if (lastModifiedAttrib == null)
         {
            return;
         }
        
         // check that date is in range
         DateTime modifiedDate = lastModifiedAttrib.DateModified;
        
         if (modifiedDate < backDateTo)
         {
            return;
         }
        
         AddToMessage(" MODIFIED: " +
            modifiedDate.ToLongDateString() + ":");
         AddToMessage(" " + lastModifiedAttrib.Changes);
        
         if (lastModifiedAttrib.Issues != null)
         {
            AddToMessage(" Outstanding issues:" +
               lastModifiedAttrib.Issues);
         }
      }

Finally, here is the helper AddToMessage method:

      static void AddToMessage(string message)
      {
         outputText.Append("
" + message);
      }
   }
}

Running this code produces the results shown in Figure 15-2.

Note that when you list the types defined in the VectorClass assembly, you actually pick up two classes: Vector and the embedded VectorEnumerator class. In addition, note that because the backDateTo date of 1 Feb is hard-coded in this example, you actually pick up the attributes that are dated 14 Feb (when you added the collection support) but not those dated 10 Feb (when you added the IFormattable interface).

SUMMARY

No chapter can cover the entire topic of reflection, an extensive subject worthy of a book of its own. Instead, this chapter illustrated the Type and Assembly classes, which are the primary entry points through which you can access the extensive capabilities provided by reflection.

In addition, this chapter demonstrated a specific aspect of reflection that you are likely to use more often than any other — the inspection of custom attributes. You learned how to define and apply your own custom attributes, and how to retrieve information about custom attributes at runtime.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset