Reflection Part 1: Discovery and Execution

Reflection Part 1: Discovery and Execution

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.

 

Looking around the .NET Framework and the various .NET tools it quickly becomes apparent that .NET itself makes extensive use of reflection. The compilers use it; other namespaces such as Remoting use it, etc. If you are like us though, you're wondering, "What can I do with reflection in my applications?"

There is no doubt that reflection does not solve everyday tasks associated with creating your average business application. You will not find yourself importing (or using) the Reflection namespace like you would the System.IO or System.Data namespaces, for example. However, once you have a solid understanding of just what reflection does, it won't be long before you'll see a number of very useful scenarios. In fact, the following are all good candidates for using reflection:

API help reference based on reflection and custom metadataType browsers and other products that work with compiled codeTesting tool that looks at an object, builds a form, and allows users to execute the code similar to a .NET Web service's .asmx fileApplications that generate codeCentralized exception handling, logging, and reportingAnytime you require late bindingWorkflow-like applications that allow users to connect components (think of a sales pipeline)

From just this short list you can already see that reflection offers a great deal of power to developers. Let's get started by looking at the basic manner in which you query for reflected information.

Reflection Discovery Basics

When working with reflection your code will typically follow a basic design pattern. You first need to load an assembly's metadata. You then search through the information to find the type or types in which you are interested. Finally, you either present this information for display or execute directly against a found type. Let's take a look at how this plays out in code.

You must first create an instance of the Reflection.Assembly class based on a known type or an assembly. We do so not with a constructor but with static (i.e. shared) "Load" methods of the Assembly class. There are a number of versions of these methods. For this example, we will focus on the following two:

  • Load(assemblyString As String) As Assembly
  • LoadFrom(assemblyFile As String) As Assembly

    Both of these methods return an instance of the Assembly class. Assembly.Load takes assemblyString as a parameter. This parameter represents the name of a loaded (in the current application's domain) assembly. For example, if you create a form-based application and then list all loaded assemblies, one of them will be "System.Drawing" which is the name of an assembly (and namespace) used by the .NET forms engine. Therefore to get an instance of this loaded assembly, you would write the following code (in VB):

    Dim myAssembly As [Assembly]
    myAssembly = [Assembly].Load("System.Drawing")

    You can also attach to your own loaded assembly. That is, your code can read reflected information about itself. To do so, you would simply use the same Load method above but replace "System.Drawing" with the name of your assembly.

    Assembly.LoadFrom works in a similar fashion. However, this method takes a string parameter that points to a .NET assembly via its path and file name. LoadForm gives you the power to work with any .NET assembly and not just those loaded into the current application domain. The following is an example:

    Dim myAssembly As [Assembly]
    Dim myPath As String
    myPath = "C:/Basics/Bin/Basics.exe"
    myAssembly = [Assembly].LoadFrom(myPath)

    Visual Basic developers can also use the keyword GetType to load an assembly. This operator takes a type as a parameter and returns a given type, which in turn provides access to its assembly data. The following is an example of GetType:

    myAssembly = GetType( _
    System.Data.DataRow).Assembly

    Of course, the .NET Framework also provides a GetType method that is available in all objects written for .NET. You can use this method as well to return a type's assembly. For example:

    Dim test As New MyTestClass()
    myAssembly = test.GetType().Assembly

    Now that you've seen numerous ways to create an instance of Reflection.Assembly, let's look at how to dig into an assembly and query the information it contains. To do so, you'll make extensive use of the System.Type class, which is used to represent all .NET types (classes, enumerations, arrays, etc.). You will use an Assembly instance to return Type instances that represent all the types in a given assembly. For example, suppose you have the following console application:

    Imports System.Reflection
    Module Basics
    Public Enum testEnum
    testValue = 1
    End Enum
    Sub Main()
    'do something
    End Sub
    End Module

    There are two types defined here at the assembly level: the module Basics and the enumeration testEnum. To access these types you call Assembly.GetTypes. This will return an array of Type objects that represent the loaded assembly's types. For example, add the following code to the Sub Main of the previous code snippet:

    Dim myAssembly As [Assembly]
    myAssembly = [Assembly].Load("Basics")
    Dim myType As Type
    For Each myType In myAssembly.GetTypes
    ' do something
    Next
    Console.ReadLine()

    You've obtained a reference to the types from the assembly with myAssembly.GetTypes and are now able to iterate through those types. You'll use these type objects to make decisions in your code. For example, you could add the following check inside the For Each...Next loop:

    If myType.IsClass Then
    Console.WriteLine(myType.Name)
    End If

    You've now successfully loaded an assembly, iterated its types, and displayed all of its classes. This represents the basic pattern you follow to work with reflected metadata on an assembly. The rest of the article will dive deeper and show you what more to do with the various Reflection classes.

    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.

    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:

  • Load the assembly.
  • Find the type or class you wish to use.
  • Create an instance of the type (or class).
  • Find a method on the type you wish to execute.
  • Get the method's parameters.
  • 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.

    Putting It All Together (A Simple Type Browser)

    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.

    Summary

    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.

    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.

    Mike Snell

    Click for a larger version of this image.

    Figure 2: Discover dialog in action.
  • Click for a larger version of this image.

    Figure 1: Visual Studio .NET Object browser.
& 

 

Fast Facts
 

.NET Reflection 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.

 

Metadata

Assembly files in .NET are made up of a manifest, metadata, MSIL code, and any resources included at compile time. The metadata portion of the file describes every member, type, dependency, interface, security permission, etc. of your executable. In fact, with metadata your components are 100% self-describing—thereby eliminating the need for component registration, header files, IDL files, etc.

Metadata is language neutral. The .NET runtime can use it to discover information about your code. Metadata allows one components written in VB to be consumed by another component written in C#.

 


Application Domains

The .NET runtime isolates your executing code inside an application domain. This domain provides complete isolation for your managed code and forms the boundaries for security. An application domain is not a process. In fact, many application domains can exist in one process.

To attach to an application domain (or to create a new domain) use the System.AppDomain class. With this class you can get a reference to all Assemblies loaded into a given application domain. For example, the CurrentDomain property returns a reference to your application's domain. You can use this property as follows to return an array of all assemblies in the given domain:

Dim a As [Assembly]

Dim as() As [Assembly]

as = AppDomain. CurrentDomain. GetAssemblies()

 


Custom Attributes

Custom attributes are attribute classes that you create and consume to describe your code. You can think of custom attributes as custom metadata. Information placed in an assembly to be consumed not by the runtime, but by another custom application. An example of a custom attribute might be a link to a help file for a given class.

You can use reflection to consume custom attributes applied to your code. To do so you use the GetCustomAttributes method of the Reflection.Assembly class.

 


Reflection Security

You may have noticed that we were able to easily access and execute private members on an Assembly. Whether or not your reflection code has access to these members is based on the security policy on the machine executing the reflection code.

If a machine restricts this access you can use the ReflectionPermission or SecurityPermisssion classes of the System. Security. Permissions namespace to gain access. These classes allow you to assert or claim certain permission on a given machine.



Listing 1: Custom type search
Imports System.Reflection
Public Class filterObject
Public criterion1 As String = "Id"
Public criterion2 As String = "Name"
End Class
Module Basics
Sub Main()
Dim memInfo As MemberInfo()
Dim i As Int16
Dim pi As PropertyInfo
Dim mf As New MemberFilter( _
AddressOf MySearchDelegate)
memInfo = GetType(SomeClass).FindMembers( _
MemberTypes.Property, _
BindingFlags.Public Or _
BindingFlags.Instance Or _
BindingFlags.GetProperty, _
mf, New filterObject())
For i = 0 To memInfo.GetUpperBound(0)
pi = CType(memInfo(i), PropertyInfo)
Console.WriteLine(pi.Name)
Next
Console.ReadLine()
End Sub
Public Function MySearchDelegate _
(ByVal m As MemberInfo, _
ByVal filterCriteria As Object) As Boolean
If m.Name = filterCriteria.criterion1 _
Or m.Name = filterCriteria.criterion2 Then
Return True
Else
Return False
End If
End Function
End Module


Listing 2: Discover form, Discover button Click event
Private Sub btnDiscover_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnDiscover.Click
' local scope
Dim currentNode As Windows.Forms.TreeNode
Dim myType As Type
Dim memberInfo As MemberInfo
Dim methodInfo As MethodInfo
Dim myMod As [Module]
Dim pi As ParameterInfo
Dim scope As String
' get path to code to discover
Dim codePath As String = txtPath.Text
If codePath <> "" Then
Try
' load an assembly using its file name
Dim myAssembly As Reflection.Assembly = _
Reflection.Assembly.LoadFrom(codePath)
' add the assembly's name to the root node
currentNode = AssemblyTree.Nodes.Add( _
Text:=myAssembly.GetName.Name)
' display details of the assembly
currentNode = currentNode.Nodes.Add("Details")
currentNode.Nodes.Add(Text:=myAssembly.FullName)
currentNode.Nodes.Add(Text:=myAssembly.Location)
' back to the assembly node
currentNode = currentNode.Parent
' add all the assembly's types
currentNode = currentNode.Nodes.Add("Types")
For Each myType In myAssembly.GetTypes
currentNode = currentNode.Nodes.Add( _
myType.FullName & " - " & _
myType.UnderlyingSystemType.ToString)
' add members for each type
currentNode = _
currentNode.Nodes.Add("All Members")
For Each memberInfo In myType.GetMembers( _
BindingFlags.Public Or BindingFlags.NonPublic _
Or BindingFlags.Instance)
currentNode.Nodes.Add( _
memberInfo.Name & " - " & _
memberInfo.MemberType.ToString)
Next
' back to the Type node
currentNode = currentNode.Parent
' add methods for each type
currentNode = currentNode.Nodes.Add("Methods")
For Each methodInfo In myType.GetMethods( _
BindingFlags.Public Or BindingFlags.NonPublic _
Or BindingFlags.Instance)
If methodInfo.IsPrivate Then
scope = "Private - "
ElseIf methodInfo.IsPublic Then
scope = "Public - "
Else
scope = ""
End If
currentNode = currentNode.Nodes.Add( _
scope & methodInfo.Name & " - " & _
methodInfo.ReturnType.ToString)
' add parameters for each method
currentNode = _
currentNode.Nodes.Add("Parameters")
For Each pi In methodInfo.GetParameters
currentNode.Nodes.Add( _
pi.Name & " - " & pi.ParameterType.Name)
Next
' back to the parameters node
currentNode = currentNode.Parent
' back to the methods node
currentNode = currentNode.Parent
Next
' back to the type node
currentNode = currentNode.Parent
' back to the types node
currentNode = currentNode.Parent
Next
Catch x As Exception
MsgBox(x.ToString)
End Try
Else
Beep()
End If
End Sub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值