Basic Instincts

Reflection in Visual Basic .NET

Ted Pattison

Contents

Reflection Fundamentals
Reflecting Against an Assembly's Type Information
Conclusion

In the May 2005 Basic Instincts column I wrote about the Microsoft® .NET Framework support for custom attributes and attribute-based programming (see Basic Instincts: Designing With Custom Attributes). I discussed how the inclusion of attributes in the .NET Framework programming model gives Microsoft and third-party developers a powerful way to extend the metadata that is compiled into assemblies. This extensibility opens up opportunities for designs that leverage attribute-based programming.

The ability to programmatically inspect an assembly at run time is important for designing software based on custom attributes. It allows you to determine how your attributes have been applied and parameterized. This month I will focus on how you can use reflection to programmatically inspect an assembly at run time. I will also take some time to discuss other scenarios in which using reflection can be helpful.

Reflection Fundamentals

Figure 1 Metadata and Type Info

Figure 1** Metadata and Type Info **

The .NET Framework introduced an important requirement to application development: every distributable unit of code (assembly) now requires high-fidelity metadata to describe itself and the types that are defined in it. Figure 1 depicts how that metadata and type information are contained within a compiled assembly.

Reflection is the act of programmatically inspecting an assembly, its metadata, and the type information that is contained within it. Getting started with reflection is quite simple. There's quite a bit you can do without writing very much code.

Reflection is made possible through a set of classes in the Framework Class Library as part of the System.Reflection namespace. Figure 2 shows an inheritance hierarchy of the classes that are commonly used when programming reflection. Note that the only two classes in this diagram that are not defined within the System.Reflection namespace are System.Object and System.Type.

Let's start by writing some code that shows you how to inspect the metadata associated with an assembly. For example, let's say you need to programmatically determine an assembly's four-part fully qualified name. You can get started by creating and initializing an object from the Assembly class.

Figure 2 Framework Class Library Reflection Classes

Figure 2** Framework Class Library Reflection Classes **

The Assembly class provides a shared method named LoadFrom that makes it possible to initialize an Assembly object using the physical path to an assembly file. Once you have initialized the Assembly object, you can query for the complete name of the assembly using the FullName property.

You might have noticed that the class name Assembly appears inside of square brackets. These square brackets are required syntax in Visual Basic® .NET because Assembly is also a keyword. You need to use the square brackets to inform the Visual Basic .NET compiler that Assembly is being used as a class name rather than a keyword.

The example in Figure 3 initializes an Assembly object using a physical path and the LoadFrom method. Alternatively, you can use the Load method when you need to initialize an Assembly object from an assembly in the Global Assembly Cache (GAC) using an assembly's fully qualified name, as shown in the following code:

Dim asm As [Assembly] Dim name As String = "AcmeCorp.BusinessLogic, " & _ "Version=1.0.24.0, " & _ "Culture=neutral, " & _ "PublicKeyToken=2bd1ac622c6e1de3" asm = [Assembly].Load(name)

Figure 3 Retrieving an Assembly Name

Imports System.Reflection Class MyTestApp Shared Sub Main() '*** determine path to assembly DLL Dim AssemblyPath As String AssemblyPath = "C:\AcmeCorp\bin\AcmeCorp.BusinessLogic.dll" '*** create and initialize Assembly object, then query for name Dim asm As [Assembly] = [Assembly].LoadFrom(AssemblyPath) Console.WriteLine("Assembly Name: " & asm.FullName) End Sub End Class

Note that the Assembly class provides three other shared methods for initializing Assembly objects in many other scenarios:

Dim asm1, asm2, asm3 As [Assembly] '*** get the Assembly which contains this code asm1 = [Assembly].GetExecutingAssembly() '*** get the Assembly with the code that called this method asm2 = [Assembly].GetCallingAssembly() '*** get the assembly for the hosting application EXE asm3 = [Assembly].GetEntryAssembly()

An Assembly object exposes the GetName method which returns an AssemblyName object. The AssemblyName object provides more granular information about the assembly name. For example, if you need to programmatically determine an assembly's simple name and version number, write the following code:

Sub GetAssemblyNameInfo(ByVal asm As [Assembly]) '*** get AssemblyName object from Assembly object Dim asmName As AssemblyName = asm.GetName() '*** query AssemblyName object Dim SimpleName As String = asmName.Name Dim VersionNumber As String = asmName.Version.ToString() Dim BuildNumber As Integer = asmName.Version.Build End Sub

You might ask why the information about an assembly's name has been factored out of the Assembly class and into the AssemblyName class. One reason has to do with the metadata contained within an assembly about its references to dependent assemblies. While the metadata for a referenced assembly does not contain enough information to initialize an Assembly object, it can provide what you need to initialize an AssemblyName object.

The metadata in every assembly contains a list of the other assemblies it references. When you reflect against an assembly, you can discover what other assemblies it references. You accomplish this by calling the GetReferencedAssemblies method of the Assembly class and enumerating through a collection of AssemblyName objects:

Shared Sub GetReferencedAssemblies(ByVal asm As [Assembly]) Dim ReferencedAssembly As AssemblyName For Each ReferencedAssembly In asm.GetReferencedAssemblies Console.WriteLine(ReferencedAssembly.FullName) Next End Sub

Reflecting Against an Assembly's Type Information

A common motivation for using reflection is to discover type information contained within an assembly at run time. This makes it possible to write utility applications that generate documentation about the public types and public methods contained within an assembly. It also makes it possible to determine how custom attributes have been applied to a class or method. Another common use is to dynamically instantiate classes and even to execute methods on those classes.

The System.Type class is the key for obtaining type information available through the Framework Class Library. When you want to programmatically discover type information, you need to acquire an instance of the System.Type class that has been initialized using a specific target type you want to inspect. An assembly object lets you enumerate through all its top-level types using a simple For Each loop to inspect System.Type objects one by one:

Shared Sub DisplayTypes(ByVal asm As [Assembly]) For Each t As System.Type In asm.GetTypes Console.WriteLine(t.Name) Next End Sub

You might wonder why the System.Type class is defined inside the System namespace while all the other reflection classes are defined within the System.Reflection namespace. One key factor that likely led to this design decision is the fact the System.Object class exposes a method named GetType which returns a System.Type instance. This means that every managed object and every managed value can provide you with an initialized System.Type instance that makes it easy to reflect against the type that was used to create it.

Let's look at a simple example. Imagine you have a reference that is holding some object and you want to determine the actual type of the object as well as the assembly in which it is contained. You can simply write a routine that looks like this:

Shared Sub DisplayTypeInfo(ByVal obj As Object) '*** get info about the object's declaring type Dim t As Type = obj.GetType() Console.WriteLine(t.FullName) '*** get info about assembly used to create object Dim asm As [Assembly] = t.Assembly Console.WriteLine(asm.FullName) End Sub

As you can see, a System.Type object is what makes it possible to obtain type information. That's because each System.Type object acts as a gateway to the other reflection classes defined inside the System.Reflection namespace.

There are several other techniques for initializing a System.Type object using a specific managed type. For example you can use the Visual Basic .NET GetType statement. When you use the GetType statement, it requires that you provide a type name:

Dim t1 As Type = GetType(Integer) Dim t2 As Type = GetType(AcmeCorp.BusinessLogic.Customer)

You can also use a shared method of the System.Type class named GetType. When calling this method you must pass a namespace-qualified type name as a string parameter (if you don't include assembly information as part of the type name, only types in the current assembly and in mscorlib.dll will be found):

Dim t As Type = System.Type.GetType("System.Int32")

One final technique for retrieving a System.Type object is to use the shared GetType method exposed by the Assembly class. This makes it possible to initialize a System.Type object from a type that you know is defined within a specific target assembly:

Dim AssemblyPath As String AssemblyPath = "C:\AcmeCorp\bin\AcmeCorp.BusinessLogic.dll" Dim asm As [Assembly] = [Assembly].LoadFrom(AssemblyPath) Dim t As Type = asm.GetType("AcmeCorp.BusinessLogic.Customer")

When you call the GetType method in this fashion, you should remember that the type name must be passed as a namespace-qualified string parameter. Also keep in mind that if the type name you pass doesn't match the name of a type within the target assembly, the call to GetType does not throw an exception. Instead, it simply returns a null reference. It is key to check the return value of GetType and see if it equals Nothing in order to determine whether you have successfully retrieved the System.Type object you're after.

Once you have acquired a System.Type object, you can start reflecting against a specific type. The System.Type class exposes many useful public instance members that can provide information about the type itself as well as the type's members. Figure 4 is a simple example of querying for information about a type.

Figure 4 Get General Type Information

Dim t As Type t = GetType(AcmeCorp.BusinessLogic.Customer) '*** display type name with namespace Console.WriteLine(t.FullName) '*** display type name without namespace Console.WriteLine(t.Name) '*** display namespace Console.WriteLine(t.Namespace) '*** determine when type is a class Console.WriteLine(t.IsClass) '*** determine when type is public Console.WriteLine(t.IsPublic) '*** display full name of containing assembly Console.WriteLine(t.Assembly.FullName) '*** display version number of containing assembly Console.WriteLine(t.Assembly.GetName.Version.ToString)

Now let's say you want to find out about the fields, methods, properties, and events defined within a type. If you refer back to Figure 2, you will see that there is a class named MemberInfo which serves as a base class to more specific reflection classes such as MethodInfo, ConstructorInfo, FieldInfo, and PropertyInfo. These classes make it possible to get detailed information about the members defined within a class.

Let's look at a simple example of reflecting against the members of a type. Once you have initialized an instance of the System.Type class, you can simply call the GetMembers method and then enumerate through the returned collection of MemberInfo objects using a For Each loop:

'*** get System.Type object for Integer and discover '*** the names of each public member Dim t As Type = GetType(AcmeCorp.BusinessLogic.Customer) For Each member As MemberInfo In t.GetMembers Console.WriteLine(member.Name) Next

When calling the GetMembers method, you can also pass parameters to narrow the scope of the returned collection. For example, if you only want to retrieve the public instance members of a type, you can refine the call to GetMembers to look like this:

'*** get System.Type object for Integer Dim t As Type = GetType(AcmeCorp.BusinessLogic.Customer) '*** get an array of public instance members Dim members As MemberInfo = _ t.GetMembers(BindingFlags.Public Or BindingFlags.Instance) '*** discover the name of each member For Each member As MemberInfo In members Console.WriteLine(member.Name) Next

If you want to retrieve a specific kind of member, you should note that the System.Type class provides several other instance methods such as GetConstructors, GetFields, GetMethods, GetProperties, GetEvents, and GetNestedTypes.

Now let's look at reflecting against a type to determine whether it has been defined with a specific attribute applied to it. In this example, I will use the sample custom attribute class named TestedAttribute that I presented in my previous column on authoring custom attributes. You can see the definition for the TestedAttribute type in the code listing in Figure 5.

Figure 5 Using the TestedAttribute Class

Public Enum TestGrade Excellent Good Poor Unacceptable End Enum <AttributeUsage(AttributeTargets.All, Inherited:=False)> _ Public Class TestedAttribute : Inherits System.Attribute Public Tester As String Public Grade As TestGrade '*** parameterized constructor for passing parameters by position Public Sub New(ByVal Tester As String, ByVal Grade As TestGrade) Me.Tester = Tester Me.Grade = Grade End Sub '*** default constructor for passing parameters by name Public Sub New() End Sub End Class

You should assume that the TestedAttribute has been applied to a custom class named Customer in the following manner:

<Tested("Bob", TestGrade.Excellent)> _ Class Customer '*** class definition omitted for brevity End Class

Now imagine it's your job to write code that reflects against classes such as the Customer class to see whether they have been defined with the TestedAttribute. Furthermore, if you determine that a class such as the Customer class has been defined with the TestedAttribute, you would like to discover how this custom attribute was parameterized when it was applied.

You should begin by learning how to leverage an instance method exposed by the System.Type class named GetCustomAttributes. This method lets you determine if and how an attribute has been applied to a type. Figure 6 shows how to use the GetCustomAttributes method to discover whether TestedAttribute has been applied to the definition of the Customer class.

Figure 6 Using GetCustomAttributes

'*** get type objects for type and custom attribute Dim t As Type = GetType(Customer) Dim attrType As Type = GetType(TestedAttribute) '*** retrieve array of custom attributes Dim results() As Object = t.GetCustomAttributes(attrType, False) '*** determine whether attribute has been applied If (results.Length >= 1) Then '*** get first element of array Dim attr As TestedAttribute = CType(results(0), TestedAttribute) '*** discover attribute parameter values Console.WriteLine("Tester: " & attr.Tester) Console.WriteLine("Grade: " & attr.Grade.ToString()) Else Console.WriteLine("TestedAttribute not present") End If

Now that you have seen all the steps at once, I'm going to examine the preceding code passage in more detail. First, you should notice that the call to GetCustomAttributes involves two System.Type objects. You must initialize one System.Type object using the type you want to reflect against and a second System.Type object based on the type of the custom attribute you are looking for, as shown here:

Dim t As Type = GetType(Customer) Dim attrType As Type = GetType(TestedAttribute) Dim results() As Object = t.GetCustomAttributes(attrType, False)

Note that the second parameter passed to GetCustomAttributes is a Boolean value that indicates whether inheritance should affect the return value of GetCustomAttributes. In this case a value of False has been passed to indicate that I'm only interested in whether the attribute has been applied directly to the Customer class. In other words, it should not affect the outcome of the call to GetCustomAttributes if one of the base classes of the Customer class has been defined with the TestedAttribute class.

Next, consider the return value of the call to GetCustomAttributes, which returns an array of references to attribute objects. When the attribute you are looking for has not been applied to the class you are inspecting, a call to GetCustomAttributes returns an array with a length of 0. Your intuition as a programmer using Visual Basic might lead you to incorrectly anticipate a return value of Nothing. Therefore, you should always inspect the length of the array to see whether the attribute has been applied or not.

If the array returned by GetCustomAttributes has a length of 1 or greater, then you know that the attribute has been applied to the target class one or more times. However, many attributes including the TestedAttribute class do not allow themselves to be applied more than once to any one target. In such cases, you know the attribute has been applied exactly once. In cases where an attribute has been defined in such a fashion that it can be applied to a target class multiple times, you must look at the array length to determine how many times it has been applied to the target class.

In my scenario with TestedAttribute, a call to GetCustomAttributes will return an array with a length of 0 or a length of 1. If the length is 0, the attribute has not been applied. If the length is 1, the array returned by GetCustomAttributes holds a reference to a freshly created instance of the TestedAttribute class that has been initialized from metadata within the assembly that defined the Customer class. This makes it easy to retrieve the TestedAttribute object from the array and query for its parameterized values:

Dim attr As TestedAttribute = CType(results(0), TestedAttribute) Console.WriteLine("Tester: " & attr.Tester) Console.WriteLine("Grade: " & attr.Grade.ToString())

Conclusion

In this month's column, I introduced reflection and explained how you can use it to get information about an assembly and the types defined inside. The kinds of application and components that are possible with reflection are only constrained by your imagination. While my May 2005 column discussed how to author custom attributes and apply them, it did not tell a complete story. To unlock the power of custom attributes in software design, it's critical that you know how to use reflection. This makes it possible to determine how your custom attributes have been applied.

Send your questions and comments for Ted to  instinct@microsoft.com.

Ted Pattison has been educating Windows-based developers as an author and trainer since 1990. Ted delivers hands-on training classes through PluralSight and provides consulting services through his company, Ted Pattison Group.