Reflection

You already know that managed applications are deployed as assemblies, that assemblies contain files that are usually (but not always) managed modules, and that managed modules contain types. You also know that every managed module has metadata inside it that fully describes the types defined in the module, and that assemblies carry additional metadata in their manifests identifying the files that make up the assembly and other pertinent information. And you鈥檝e seen how ILDASM can be used to inspect the contents of an assembly or managed module. Much of the information that ILDASM displays comes straight from the metadata.

The System.Reflection namespace contains types that you can use to access metadata without having to understand the binary metadata format. The term 鈥渞eflection鈥?means inspecting metadata to get information about an assembly, module, or type. The .NET Framework uses reflection to acquire important information at run time about the assemblies that it loads. Visual Studio .NET uses reflection to obtain IntelliSense data. The managed applications that you write can use reflection, too. Reflection makes the following operations possible:

Not every managed application uses reflection or has a need to use reflection, but reflection is something every developer should know about, for two reasons. First, learning about reflection deepens one鈥檚 understanding of the .NET Framework. Second, reflection can be extraordinarily useful to certain types of applications. While far from exhaustive, the next few sections provide a working introduction to reflection and should at least enable you to hold your own when the conversation turns to reflection at a .NET party.

Retrieving Information About Assemblies, Modules, and Types

One use for reflection is to gather information at run time about assemblies, managed modules, and the types that assemblies and modules contain. The key classes that expose the functionality of the framework鈥檚 reflection engine are

The first step in acquiring information from an assembly鈥檚 metadata is to load the assembly. The following statement uses the static Assembly.LoadFrom method to load the assembly whose manifest is stored in Math.dll:

Assembly聽a聽=聽Assembly.LoadFrom聽("Math.dll");

LoadFrom returns a reference to an Assembly object representing the loaded assembly. A related method named Load takes an assembly name rather than a file name as input. Once an assembly is loaded, you can use Assembly methods to retrieve all sorts of interesting information about it. For example, the GetModules method returns an array of Module objects representing the modules in the assembly. GetExportedTypes returns an array of Type objects representing the types exported from the assembly (in other words, the assembly鈥檚 public types). GetReferencedAssemblies returns an array of AssemblyName objects identifying assemblies used by this assembly. And the GetName method returns an AssemblyName object that serves as a gateway to still more information encoded in the assembly manifest.

Figure 3-9 contains the source code listing for a console application named AsmInfo that, given the name of a file containing an assembly manifest, uses reflection to display information about the assembly. Included in the output is information indicating whether the assembly is strongly or weakly named, the assembly鈥檚 version number, the managed modules that it consists of, the types exported from the assembly, and other assemblies containing types that this assembly references. When run on the weakly named version of the Math assembly (Math.dll) presented in Chapter 2, AsmInfo produces the following output:

Naming:聽Weak
Version:聽0.0.0.0

Modules
聽聽math.dll
聽聽simple.netmodule
聽聽complex.netmodule

Exported聽Types
聽聽SimpleMath
聽聽ComplexMath

Referenced聽Assemblies
聽聽mscorlib
聽聽Microsoft.VisualBasic

You can plainly see the two types exported from the Math assembly (SimpleMath and ComplexMath) and the modules that make up the assembly (Math.dll, Simple.netmodule, and Complex.netmodule). Mscorlib appears in the list of referenced assemblies because it contains the core data types used by virtually all managed applications. Microsoft.VisualBasic shows up also because one of the modules in the assembly, Simple.netmodule, was written in Visual Basic .NET.

AsmInfo.cs
using聽System;
using聽System.Reflection;

class聽MyApp
{
聽聽聽聽static聽void聽Main聽(string[]聽args)
聽聽聽聽{
聽聽聽聽聽聽聽聽if聽(args.Length聽==聽0)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Error:聽Missing聽file聽name");
聽聽聽聽聽聽聽聽聽聽聽聽return;
聽聽聽聽聽聽聽聽}

聽聽聽聽聽聽聽聽try聽{
聽聽聽聽聽聽聽聽聽聽聽聽//聽Load聽the聽assembly聽identified聽on聽the聽command聽line
聽聽聽聽聽聽聽聽聽聽聽聽Assembly聽a聽=聽Assembly.LoadFrom聽(args[0]);
聽聽聽聽聽聽聽聽聽聽聽聽AssemblyName聽an聽=聽a.GetName聽();

聽聽聽聽聽聽聽聽聽聽聽聽//聽Indicate聽whether聽the聽assembly聽is聽strongly聽or
聽聽聽聽聽聽聽聽聽聽聽聽//聽weakly聽named
聽聽聽聽聽聽聽聽聽聽聽聽byte[]聽bytes聽=聽an.GetPublicKeyToken聽();
聽聽聽聽聽聽聽聽聽聽聽聽if聽(bytes聽==聽null)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Naming:聽Weak");
聽聽聽聽聽聽聽聽聽聽聽聽else
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Naming:聽Strong");

聽聽聽聽聽聽聽聽聽聽聽聽//聽Display聽the聽assembly's聽version聽number
Figure 3-9
AsmInfo source code.
聽聽聽聽聽聽聽聽聽聽聽聽Version聽ver聽=聽an.Version;
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Version:聽{0}.{1}.{2}.{3}",
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽ver.Major,聽ver.Minor,聽ver.Build,聽ver.Revision);

聽聽聽聽聽聽聽聽聽聽聽聽//聽List聽the聽modules聽that聽make聽up聽the聽assembly
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("\nModules");
聽聽聽聽聽聽聽聽聽聽聽聽Module[]聽modules聽=聽a.GetModules聽();
聽聽聽聽聽聽聽聽聽聽聽聽foreach聽(Module聽module聽in聽modules)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("  " +聽module.Name);

聽聽聽聽聽聽聽聽聽聽聽聽//聽List聽the聽types聽exported聽from聽the聽assembly
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("\nExported聽Types");
聽聽聽聽聽聽聽聽聽聽聽聽Type[]聽types聽=聽a.GetExportedTypes聽();
聽聽聽聽聽聽聽聽聽聽聽聽foreach聽(Type聽type聽in聽types)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("  " +聽type.Name);

聽聽聽聽聽聽聽聽聽聽聽聽//聽List聽assemblies聽referenced聽by聽the聽assembly
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("\nReferenced聽Assemblies");
聽聽聽聽聽聽聽聽聽聽聽聽AssemblyName[]聽names聽=聽a.GetReferencedAssemblies聽();
聽聽聽聽聽聽聽聽聽聽聽聽foreach聽(AssemblyName聽name聽in聽names)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("  " +聽name.Name);
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽catch聽(Exception聽e)聽{
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽(e.Message);
聽聽聽聽聽聽聽聽}
聽聽聽聽}
}

If you鈥檇 like to know even more about an assembly鈥攕pecifically, about the modules that it contains鈥攜ou can use the Module objects returned by Assembly.GetModules. Calling GetTypes on a Module object retrieves a list of types defined in the module鈥攁ll types, not just exported types. The following code sample writes the names of all the types defined in module to a console window:

Type[]聽types聽=聽module.GetTypes聽();
foreach聽(Type聽type聽in聽types)
聽聽聽聽Console.WriteLine聽(type.FullName);

To learn even more about a given type, you can call GetMembers on a Type object returned by GetTypes. GetMembers returns an array of MemberInfo objects representing the type鈥檚 individual members. MemberInfo.MemberType tells you what kind of member a MemberInfo object represents. MemberTypes.Field, for example, identifies the member as a field, while MemberTypes.Method identifies it as a method. A MemberInfo object鈥檚 Name property exposes the member鈥檚 name. Using these and other Type members, you can drill down as deeply as you want to into a type, even identifying the parameters passed to individual methods (and the methods鈥?return types) if you care to.

Using reflection to inspect the contents of managed executables is probably only interesting if you plan to write diagnostic utilities. But the fact that reflection exists at all leads to some other interesting possibilities, one of which is discussed in the next section.

Custom Attributes

One of the ground-breaking new language features supported by CLR-compliant compilers is the attribute. Attributes are a declarative means for adding information to metadata. For example, if you attribute a method this way and compile it without a 鈥淒EBUG鈥?symbol defined, the compiler emits a token into the module鈥檚 metadata noting that DoValidityCheck can鈥檛 be called:

[Conditional聽("DEBUG")]
public聽DoValidityCheck聽()
{
聽聽...
}

If you later compile a module that calls DoValidityCheck, the compiler reads the metadata, sees that DoValidityCheck can鈥檛 be called, and ignores statements that call it.

Attributes are instances of classes derived from System.Attribute. The FCL defines several attribute classes, including System.Diagnostics.ConditionalAttribute, which defines the behavior of the Conditional attribute. You can write attributes of your own by deriving from Attribute. The canonical example of a custom attribute is a CodeRevision attribute for documenting source code revisions. Revisions noted with source code comments鈥攁 traditional method for documenting code revisions鈥攁ppear only in the source code. Revisions noted with attributes, however, are written into the compiled executable鈥檚 metadata and can be retrieved through reflection.

Here鈥檚 the source code for a custom attribute named CodeRevisionAttribute:

[AttributeUsage聽(AttributeTargets.All,聽AllowMultiple=true)]
class聽CodeRevisionAttribute聽:聽Attribute
{
聽聽聽聽public聽string聽Author;
聽聽聽聽public聽string聽Date;
聽聽聽聽public聽string聽Comment;

聽聽聽聽public聽CodeRevisionAttribute聽(string聽Author,聽string聽Date)聽
聽聽聽聽{
聽聽聽聽聽聽聽聽this.Author聽=聽Author;
聽聽聽聽聽聽聽聽this.Date聽=聽Date;
聽聽聽聽}

}

The first statement, AttributeUsage, is itself an attribute. The first parameter passed to it, AttributeTargets.All, indicates that CodeRevisionAttribute can be applied to any element of the source code鈥攖o classes, methods, fields, and so on. The second parameter permits multiple CodeRevisionAttributes to be applied to a single element. The remainder of the code is a rather ordinary class declaration. The class constructor defines CodeRevisionAttribute鈥檚 required parameters. Public fields and properties in an attribute class can be used as optional parameters. Because CodeRevisionAttribute defines a public field named Comment, for example, you can include a comment string in a code revision attribute simply by prefacing the string with 鈥淐omment=.鈥?/p>

Here鈥檚 an example demonstrating how CodeRevisionAttribute might be used:

[CodeRevision聽("billg", "07-19-2001")]
[CodeRevision聽("steveb", "09-30-2001",聽Comment="Fixed聽Bill's聽bugs")]
struct聽Point
{
聽聽聽聽public聽int聽x;
聽聽聽聽public聽int聽y;
聽聽聽聽public聽int聽z;
}

Get the picture? You can attribute any element of your source code by simply declaring a CodeRevisionAttribute in square brackets. You don鈥檛 have to include the word Attribute in the attribute name because the compiler is smart enough to do it for you.

Reflection is important to developers who write (or use) custom attributes because it is through reflection that an application reads information added to its (or someone else鈥檚) metadata via custom attributes. The following code sample enumerates the code revision attributes attached to type Point and writes them to the console window. Enumeration is made possible by MemberInfo.GetCustomAttributes, which reads the custom attributes associated with any element that can be identified with a MemberInfo object:

MemberInfo聽info聽=聽typeof聽(Point);
object[]聽attributes聽=聽info.GetCustomAttributes聽(false);

if聽(attributes.Length聽>聽0)聽{
聽聽聽聽Console.WriteLine聽("Code聽revisions聽for聽Point聽struct");
聽聽聽聽foreach聽(CodeRevisionAttribute聽attribute聽in聽attributes)聽{
聽聽聽聽聽聽聽聽Console.WriteLine聽("\nAuthor:聽{0}",聽attribute.Author);
聽聽聽聽聽聽聽聽Console.WriteLine聽("Date:聽{0}",聽attribute.Date);
聽聽聽聽聽聽聽聽if聽(attribute.Comment聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽Console.WriteLine聽("Comment:聽{0}",聽attribute.Comment);
聽聽聽聽}
}

And here鈥檚 the output when this code is run against the Point struct shown above:

Code聽revisions聽for聽Point聽struct

Author:聽billg
Date:聽07-19-2001

Author:聽steveb
Date:聽09-30-2001
Comment:聽Fixed聽Bill's聽bugs

Writing a reporting utility that lists all the code revisions in a compiled executable wouldn鈥檛 be difficult because types and type members are easily enumerated using the reflection techniques described in the previous section.

Dynamically Loading Types (Late Binding)

A final use for reflection is to dynamically load types and invoke methods on them. 鈥淒ynamic loading鈥?means binding to a type at run time rather than compile time. Let鈥檚 say your source code references a type in another assembly, like this:

Rectangle聽rect聽=聽new聽Rectangle聽();

Here you鈥檙e practicing early binding because your compiler inserts data into the resulting executable, noting that a type named Rectangle is imported from another assembly. Late binding gives you the ability to use a type without embedding references to it in your metadata. Late binding is accomplished by using reflection.

One use for late binding is to facilitate plug-ins. Suppose you want to enable third-party developers to extend your application by contributing images to the splash screen your app displays when it starts up. Because you don鈥檛 know in advance what plug-ins might be present at startup, you can鈥檛 early bind to classes in the plug-ins. But you can late bind to them. If you instruct third-party developers to build classes named PlugIn, and if each Plug颅In class contains a method named GetImage that returns an image to the caller, the following code calls GetImage on each plug-in represented in the list of assembly names in the names array:

ArrayList聽images聽=聽new聽ArrayList聽();

foreach聽(string聽name聽in聽names)聽{
聽聽聽聽Assembly聽a聽=聽Assembly.Load聽(name);
聽聽聽聽Type聽type聽=聽a.GetType聽("PlugIn");
聽聽聽聽MethodInfo聽method聽=聽type.GetMethod聽("GetImage");
聽聽聽聽Object聽obj聽=聽Activator.CreateInstance聽(type);
聽聽聽聽Image聽image聽=聽(Image)聽method.Invoke聽(obj,聽null);
聽聽聽聽images.Add聽(image);
}

At the end, the ArrayList named images holds an array of Image objects representing the images obtained from the plug-ins.

Visual Basic .NET uses late binding to interact with variables whose declared type is Object. Late binding is an important part of the .NET Framework architecture and something you should be aware of even if you don鈥檛 use it.