devxlogo

Reflection Part I: Discovery and Execution

Reflection Part I: Discovery and Execution

eflection in .NET is an exciting new tool for most developers. This article shows you how to use reflection to discover objects at runtime that you did not know existed at design time, create an instance of those objects, and execute methods of those objects.

What Is Reflection?
Reflection is a means of discovering information about objects at runtime. This information can be used to execute methods and retrieve property values of these objects in a dynamic manner. Code written for the .NET Framework automatically reflects, or describes, itself. It does so via compiler-generated metadata. You can therefore use reflection in your applications to query managed code about its types (including class names, methods, and parameters) and then interact with this information?this includes executing the discovered code. You can also use reflection to generate code at runtime, compile it, and run it.

What’s the Point of Reflection?
The most obvious example of reflection is a type or object browser. Think of the Visual Studio .NET object browser. This utility can display classes exposed by an assembly, methods on those classes, parameters for those methods, etc. (see Figure 1 for an example). In the past, some of this information was available to us via COM type libraries. With .NET this application data is available from the assembly itself. All .NET assemblies are self-describing?that is, they contain metadata about their types. You can query this metadata to learn about a given object. (See Sidebar: Metadata)

The Namespaces and Classes
There are two namespaces that contain the Reflection classes, System.Reflection and System.Reflection.Emit. The Reflection namespace contains objects related to type discovery and execution while the Reflection.Emit namespace dynamically generates code at runtime (this is the focus of part 2 of this article). Table 1 provides a quick and easy reference to the key classes you’ll use when working with reflection.

Reflection Discovery: Searching and Filtering
Earlier in this article you learned how to load an assembly and gain access to its types. In fact, for any given .NET assembly you can return all of its types using Assembly.GetTypes. This method returns all of the global or public types (depending on your .NET security model or context) stored in a given assembly. For example, the following code creates an Assembly instance based on itself and iterates through the assembly’s types:

   Imports System.Reflection   Module Basics      Sub Main()         Dim mi As MethodInfo         Dim myAssembly As [Assembly]         myAssembly = GetType(Basics).Assembly         Dim t As Type         For Each t In myAssembly.GetTypes()            Console.WriteLine("Type:=" & t.Name)         Next         Console.ReadLine()      End Sub   End Module

This code is useful for looping through all of the public types exposed on a given assembly, but what if you have a large assembly or are simply after specific types such as a constructor or a property? If you’ve spent any time at all looking at the .NET Framework Class Library, you know that one Assembly can contain a large number of types. You may not need to know every type; you might prefer to filter or search for a very specific type or types. Fortunately, the System.Type class exposes a number of methods for accessing, filtering, and searching for specific types inside a given assembly.

Direct Access
Direct access to a given type implies you’ve made a decision and are now looking for an exact type. Perhaps your application queries the user for the type they are interested in, or perhaps you knew from the start the specific type to look for. In any case, the System.Type class provides a number of methods that provide direct access to specific types. Methods like GetConstructor, GetMethod, GetProperty, and GetEvent allow you to target their specific types. For example, suppose you have the following class:

   Public Class SomeClass      Public Sub New()      End Sub         Public Sub New(ByVal someValue As Int32)      End Sub         Public Sub New(ByVal someValue As Int32, _         ByVal someOtherValue As Int32)      End Sub         Public Sub SomeMethod()      End Sub   End Class

Here is a class with three empty constructors and an empty method. Now assume that you want to access the constructor that takes one integer value as its only parameter. To do so you would use the GetConstructor method of the Type class. This method allows you to pass an array of objects that represent parameters for a given constructor. When executed, the method searches a type for any constructor that matches the signature defined by the array of parameters. GetConstructor then returns a ConstructorInfo object for your use (perhaps you want to invoke the constructor). For example, you would first create the array of parameters as follows:

   Dim ts() As Type = {GetType(Int32)}

Finally, you would then call GetConstructor of your Type object as follows:

   Dim ci As ConstructorInfo = _   GetType(SomeClass).GetConstructor(ts)

Similarly, Type.GetMethod provides you with direct access to the methods on a given object.

This method returns a MethodInfo instance for your use. It simply takes the name of the given method as a parameter. For example:

   Dim mi As MethodInfo = _   GetType(SomeClass).GetMethod("SomeMethod")

You might wonder, “What if I have two methods with the same name but different signatures in the same class?” Well, in the previous case you would get an ambiguous matching error. However, there are a number of versions of all of these direct access methods that allow you to pinpoint specific types. For instance the GetConstructor method can filter methods based on an array of parameters; or you could filter based on calling conventions, return type, etc. You can apply this same pattern to directly access a specific property or an event contained by your type.

Filtering
The System.Type class also provides a number of methods for returning a filtered set of types contained inside a class or another type. The methods GetConstructors, GetMethods, GetProperties, and GetEvents allow you to either return all of the given types as an array or supply filter criteria to only return a specific set of types.

A typical filter involves setting binding flags. Binding flags represent search criteria. You use values of the BindingFlags enumeration to represent things such as public or non-public types. You can also indicate flags or static members. You can even combine flags to further narrow your search. Consider the following basic class:

   Public Class SomeClass         Public Sub SomeMethod()      End Sub         Public Shared Sub SomeSharedMethod()      End Sub         Public Shared Sub SomeOtherSharedMethod()      End Sub      End Class

You can see that there are three public methods, two of which are static (or shared). Suppose that through reflection you want to find all public static methods for the SomeClass Type. You would call GetMethods and pass BindingFlag enumeration values such as the bindingAttr parameter. The following code retrieves an array of all methods in SomeClass that are both public and static:

   Dim mi As MethodInfo() = _   GetType(SomeClass).GetMethods( _   BindingFlags.Public Or BindingFlags.Static)

You can use this same technique to return private types (provided you have the correct permission). To do so you would use the BindingFlag enumeration value BindingFlags.NonPublic. This can be combined with BindingFlags.Instance to return all instance methods that are private. This same pattern can be applied to return constructors, properties, and events.

Searching
As you may have guessed, searching is a very similar process to filtering. The only real difference is that searching is done via a more abstract method of System.Type: FindMembers. Rather than call a specific filter such as GetEvents you might use FindMembers and pass the value MemberTypes.Events as the memberType parameter. If you need a custom filter that doesn’t involve a specific type, you can use the parameters of FindMembers to satisfy a number of different searching requirements. Here’s an example.

Notice that the following Visual Basic class defines three fields: two private and one public.

   Public Class SomeClass      Private myPrvField1 As Int32 = 15      Private myPrvField2 As String = _         "Some private field"      Public myPubField1 As Decimal = 1.03   End Class

Suppose that you need to use the FindMembers method of the Type class to return the private fields on an instance of SomeClass and display their values. You would do so by indicating the value Field of the enumeration MemberTypes for the memberTypes parameter of FindMembers. You then can indicate your BindingFlags, and FindMembers will return an array of MemberInfo objects that match your search criteria. The following code snippet provides an example:

   Dim memInfo As MemberInfo()   Dim fi As FieldInfo   Dim sc As New SomeClass()   memInfo = sc.GetType.FindMembers( _      MemberTypes.Field, BindingFlags.NonPublic Or _      BindingFlags.Instance, Nothing, Nothing)   Dim i As Int16   For i = 0 To memInfo.GetUpperBound(0)      fi = CType(memInfo(i), FieldInfo)      Console.WriteLine(fi.GetValue(sc))   Next

Once you find the target members, the code converts them into actual FieldInfo objects and is able to query them for their values.

Custom Searching
Even with all the aforementioned type searching capabilities you may find that you need to define a custom type search. Custom searching involves the method from the previous example, FindMembers. You may have noticed that in the previous example we set two of FindMembers’ parameters (filter and filterCriteria) to Nothing. The filter parameter takes an instance of the MemberFilter delegate. Defining a delegate allows you to define custom search logic. This delegate simply receives a MemberInfo object that represents a member that meets all the other search criteria.

The filterCriteria parameter can be any .NET object. You can use this parameter to define your custom search criteria. This can be as simple as a string or as involved as a custom object. Here is an example.

Image you have a class called SomeClass and that it defines three properties: Name, Id, and Type. Now suppose that you need a filter that searches classes to find only the properties Name and Id. We’ve provided a very simple example and of course you could return all properties, loop through them, and filter out those not named Name or Id. Take a look at the console application in Listing 1.

This application defines the delegate, MySearchDelegate, to customize the search. It creates the custom object, filterObject, with two fields that help define our search criteria. The application then calls FindMembers and indicates that you want all Property types. When a Property type is found the application raises the MySearchDelegate and passes the filterCriteria instance. The delegate then simply makes a decision based on the member name and returns True or False indicating whether or not the search passed the custom test. (See Sidebar: Custom Attributes)

Executing Discovered Code
Discovering types at runtime is great but being able to act on those types provides the real power behind reflection. With reflection you can write code that has no idea of a given object or assembly at design time. The code you write can then create an instance of a class within a discovered assembly, find a method on that class, get the method’s parameters, and execute the method. This is the ultimate in late binding: locating and executing a type at runtime. In fact, when you use basic late binding in VB, the compiler uses reflection implicitly. Let’s look at how to handle this explicitly.

The process for executing discovered code follows these basic steps:

  1. Load the assembly.
  2. Find the type or class you wish to use.
  3. Create an instance of the type (or class).
  4. Find a method on the type you wish to execute.
  5. Get the method’s parameters.
  6. Invoke the object’s method and pass the proper parameters.

You’ve already seen how to load an assembly and search for types in that assembly. Once you know the type you are after, you can use the System.Activator class to return an instance of the type. You’ll use one of the CreateInstance methods of the Activator class. CreateInstance allows you to specify the object you want created and optionally the parameters used in the object’s constructors. Here is a simple example where an object’s default constructor takes no parameters:

   Dim obj As Object = _      Activator.CreateInstance(myType)

Suppose that you want to create an instance of a given object with a constructor that took parameters. You can do so by passing these values as an array to CreateInstance. Each value needs to be of the same type and in the same order of the constructor’s signature. Imagine that the type you were trying to create had the following constructor:

   Public Sub New(ByVal someParam As String)

Your first job would be to query the constructor for parameters. Once you’ve identified the constructor, you can get its parameters using the GetParameters method of the ConstructorInfo class. GetParameters will return an array of ParameterInfo objects that will let you determine a parameter’s order, its name, and its data type. You can then build your own array of parameter values and pass it to CreateInstance. Here is a basic example.

Suppose you have a class called SomeClass that has a constructor that takes one parameter that is of the type String. Let’s also suppose you have a reference to SomeClass called myType. Finally, let’s assume you have a reference (called ci) to SomeClass’ constructor as a ConstructorInfo instance. To get a list of parameters for the given constructor (ci) you call GetParameters as follows:

   Dim pi() As ParameterInfo   pi = ci.GetParameters()

Then you create your own array of the same size as the number of parameters returned by GetParameters.

   Dim params(pi.GetUpperBound(0)) As Object

Values are set for every parameter in the array:

   Dim i As Int16   For i = 0 To pi.GetUpperBound(0)      If pi(i).ParameterType.Name = "String" Then         params(i) = "Test"      End If   Next

Finally, you call CreateInstance, passing in both your type and the values for the type’s constructor’s parameters.

   Dim o As Object = _   Activator.CreateInstance(myType, params)

Now that you have an instance (o) for your object (SomeClass), let’s look at how you might execute one of its methods. The same process of querying for parameters and passing them into a constructor works for methods. Let’s suppose that SomeClass has a method called SomeMethod that you want to invoke. To keep this simple let’s suppose that SomeMethod takes no parameters (as this is the same process outlined above). To invoke SomeMethod you need to get a reference to the method as a MethodInfo object. You can search for methods on your type with either GetMethod or GetMethods. Let’s use GetMethod and pass the method name as a string:

   Dim mi As MethodInfo = _   t.GetMethod("SomeMethod")

You have both an instance to SomeClass and a reference (mi) to the method you wish to call so you can use MethodInfo.Invoke to call your target method. You’ll pass your object instance that contains your method and an array of parameters that the method takes (in this case Nothing). For example:>

   mi.Invoke(o, Nothing)

You’ve now successfully created an instance of an object not necessarily known at design time, found a method on that object, and invoked the method. You can easily extrapolate this example to create a utility such as a testing tool. Suppose that you allow a user to select an assembly. You could list all classes and methods in the given assembly. The user could select a method at run time. You could use discovered information about the class and method to present the user with a form to exercise the method. The user could then enter values for the given method and the testing tool would invoke the method and return the results?all without knowing a thing about the assembly at design time. (See Sidebar: Reflection Security)

Putting It All Together (A Simple Type Browser)

Figure 2: Discover dialog in action.

To show how easy it is to create a .NET type browser using reflection we put together a simple application called Discover that will allow you to browse a directory for any stored assembly (.DLL or .EXE). Once you select an assembly, you can click the Discover button and the application will fill a Tree control with information about that assembly. Figure 2 shows the Discover utility in action.

The information that the Discover application displays is all types in a given assembly, all the members of a given type, all the methods of a given type, and each method’s parameters and data types. Displaying this information with reflection is remarkably easy. Listing 2 presents the code that loads the assembly and builds the tree. This code should be familiar to you, as it has been presented in previous sections of this article.

Hopefully the information in this article will allow you to add reflection as a new tool to your programmer’s tool belt. It may not be the tool you’ll reach for in typical programming situations, but given the right task, it can provide you with a lot of power.

In Part 2 of our reflection series we’ll explore using the System.Reflection.Emit namespace to generate code at runtime.

All of the code for this article can be downloaded from www.brilliantstorm.com/resources.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist

©2024 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.