Chapter 46. Reflection

There are situations in which you need to implement logic for performing some tasks depending on user choices. This kind of a situation is not uncommon. The problem is when you cannot predetermine the code required for executing actions depending on user input. Think of code generators: These tools know how to generate code but cannot predetermine which code has to be generated until the users specify their requirements. Also think of assemblies external from your application. In some cases you might want to use types from an external assembly; in other cases you might just want to get information on types provided by the assembly; and in still other cases you might want to reach members with limited scope visibility that you could not reach by adding a reference. Reflection is a key part in the .NET Framework that enables you to accomplish all these mentioned scenarios. In this chapter, you learn to use Reflection to both inspect assemblies and types and to generate and consume code on-the-fly. Also, in the final part of the chapter you learn about a new feature in Visual Basic 2012 known as Caller Information. It enables you to retrieve information that in the past was available only via advanced Reflection techniques and that is now easier to get.

Introducing Reflection

Reflection is an important part of the .NET Framework and provides the capability for interrogating assemblies’ metadata and collecting information on types exposed by assemblies. Reflection also enables you to invoke code from external assemblies and generate code on-the-fly. You can take advantage of Reflection by using objects exposed by the System.Reflection namespace. It can be particularly useful when you need to generate code according to some user input or when you are in late bound scenarios where making decisions on which code must be invoked (or generated) is something determined at runtime. Before putting your hands on code, it is necessary to get an overview of how assemblies are structured so that you can have a better understanding of the type of information you can investigate with the Reflection.

Understanding Assemblies’ Metadata

As you know, when you build an executable with Visual Basic, you build a .NET assembly. An assembly is a container of metadata and code. Metadata is information that the Common Language Runtime (CLR) uses in correctly loading and running the assembly. Figure 46.1 represents how an assembly is structured.

Image

Figure 46.1. How an assembly is structured.

The Assembly Metadata, also known as assembly manifest, provides assembly information such as the name, version, culture, copyright information, and signature. The Type Metadata contains information on types defined within the assembly, such as class names and names of class members, including their parameters. The Code part is the actual Intermediate Language code that will be executed when the assembly is loaded. The Resources block contains all resources required by the assembly, such as images, icons, and strings. Additionally, types within an assembly can be grouped into multiple modules. A module is a container of types, whereas an assembly is a container of modules. With Reflection, you can inspect metadata and code from an assembly using Visual Basic code, including assembly information.


Note

When talking about assemblies, we usually refer to single file executables. Assemblies can be composed of multiple linked files; keep in mind that assembly metadata needs to reside only in the main assembly. This is a special case and cannot be accomplished with Visual Studio (you should manually use MSBuild), but it is something that it is worth mentioning.


Preparing a Sample Assembly

Before showing Reflection capabilities, a good idea is to prepare an appropriate code example. First, create a new class library project and name it People. The goal of the library is to expose a special implementation of the Person class, with interfaces and enumeration implementations for a better demonstration on Reflection. When ready, write the code in Listing 46.1, which is quite simple.

Listing 46.1. Preparing Code for Reflection


Imports System.Text

Public Enum Genders
    Male = 0
    Female = 1
End Enum

Public Interface IPerson
    Property FirstName As String
    Property LastName As String
    Property Age As Integer
    Property Gender As Genders
    Event InstanceCreated()
    Function BuildFullName() As String
End Interface

Public Class Person
    Implements IPerson

    Public Property FirstName As String Implements IPerson.FirstName
    Public Property Gender As Genders Implements IPerson.Gender
    Public Property LastName As String Implements IPerson.LastName
    Public Property Age As Integer Implements IPerson.Age
    Public Event InstanceCreated() Implements IPerson.InstanceCreated

    Public Overridable Function BuildFullName() As String _
                       Implements IPerson.BuildFullName

        Dim fullName As New StringBuilder
        fullName.Append(LastName)
        fullName.Append(" ")
        fullName.Append(FirstName)
        fullName.Append(", ")
        fullName.Append(Gender.ToString)
        fullName.Append(", of age ")
        fullName.Append(Age.ToString)
        Return fullName.ToString
    End Function
End Class


Build the project; then add a new Console project to the current solution. Finally, add a reference to the People class library so that—just for demo purposes—you can load the assembly for Reflection without specifying the full path.

Getting Assembly Information

You get assembly metadata information by creating an instance of the System.Reflection.Assembly class. This class provides both static and instance members for accessing assembly information. Typically, you use one of the methods summarized in Table 46.1 to load an assembly for getting information.

Table 46.1. Methods for Loading an Assembly

Image

When you get the instance of the assembly you want to inspect, you can access information via some useful properties. The code in Listing 46.2 shows how to accomplish this. (See comments for explanations.)

Listing 46.2. Inspecting Assembly Information


Imports System.Reflection

Module GettingAsmInfo

    Sub Main()
        'Infers System.Reflection.Assembly
        Dim asm = Assembly.ReflectionOnlyLoadFrom("People.dll")

        With asm
            'Gets the full assembly name with
            'version and culture
            Console.WriteLine("Assembly name:")
            Console.WriteLine(.FullName)
            'Gets whether the assembly is fully trusted
            Console.WriteLine("Is full-trust: {0}", .IsFullyTrusted)
            'Gets the assembly entry point. If empty, the
            'constructor is the entry point
            Console.WriteLine("The entry point method is: {0}", .EntryPoint)
            'Gets the .NET version that the
            'assembly was built upon
            Console.WriteLine("Image runtime version: {0}", .ImageRuntimeVersion)
            'Gets whether the assembly was loaded from
            'the GAC
            Console.WriteLine("Loaded from the GAC: {0}", .GlobalAssemblyCache)
            'Gets the assembly location
            Console.WriteLine("Assembly path: {0}", .Location)

            'Gets an array of modules loaded
            'by the assembly
            Console.WriteLine("Loaded modules: ")
            For Each item As System.Reflection.Module _
                In .GetLoadedModules
                Console.WriteLine("   {0}", item.Name)
            Next
        End With
        Console.ReadLine()
    End Sub
End Module


Notice how the code uses the ReflectionOnlyLoadFrom method to enable only inspection without code execution capabilities. If you run the preceding code, you get the following result:

Assembly name:
People, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Is full-trust: True
The entry point method is:
Image runtime version: v4.0.30319
Loaded from the GAC: False
Assembly path: C:UsersAlessandrodocumentsvisual studio
2012ProjectsReflectionReflectioninDebugPeople.dll
Loaded modules:
      People.dll

The Assembly.GetModules method returns an array of modules loaded by the instance of the assembly. Other interesting methods are GetExportedTypes, which returns an array of publicly visible types, and GetFiles, which returns an array of FileStream objects, each representing a file in the assembly’s resources. Inspecting assembly information is just the first level of Reflection. The next step is inspecting types.

Reflecting Types

Reflection enables retrieving information on programs, including modules, types, and type members defined within an assembly. For example, you might want to enumerate all types and type members defined in the People.dll assembly. Take a look at the following code:

Dim asm = Assembly.LoadFrom("People.dll")

Console.WriteLine("Enumerating types:")
For Each t In asm.GetTypes
    Console.WriteLine("Type name: {0}", t.ToString)

    Console.WriteLine(" Constructors:")
    For Each constructor In t.GetConstructors
        Console.WriteLine("     " + constructor.ToString)
    Next

    Console.WriteLine(" Methods:")
    For Each method In t.GetMethods
        Console.WriteLine("     " + method.ToString)
    Next

    Console.WriteLine(" Properties:")
    For Each [property] In t.GetProperties
        Console.WriteLine("     " + [property].ToString)
    Next

    Console.WriteLine(" Fields:")
    For Each field In t.GetFields
        Console.WriteLine("     " + field.ToString)
    Next

    Console.WriteLine(" Events:")
    For Each [event] In t.GetEvents
        Console.WriteLine("     " + [event].ToString)
    Next
Next

You still get the instance of the desired assembly; then you can iterate types (or modules if preferred). The Assembly.GetTypes method returns an array of System.Type objects defined in the assembly that you can iterate for detailed information. The System.Type class exposes several GetX methods, in which X can stand for Constructors, Properties, Methods, Fields, and Events. Each of these methods returns an XInfo class instance, such as MethodInfo, PropertyInfo, FieldInfo, and so on. Each class exposes interesting properties about the inspected member for further information such as IsPrivate, IsPublic, or IsStatic.


Using ToString

Each XInfo class also exposes a Name property that returns the name of the member. In this case, ToString was used instead of the name to return the full member signature.


Also, the System.Type class offers some useful properties enabling you to understand what kind of type you are inspecting such as IsClass, IsInterface, or IsEnum. The Namespace property enables you to get the namespace exposing the inspected type. Notice that the preceding code inspects all types defined in the specified assembly, including the ones that are usually part of My Project. Also notice that Reflection considers properties’ getters and setters such as methods that thus will be listed within this category. For a better understanding, the following is an excerpt of the output produced by the previously illustrated code:

Enumerating types:
Type name: People.My.MyApplication
 Constructors:
     Void .ctor()
 Methods:
     System.String GetEnvironmentVariable(System.String)
     Microsoft.VisualBasic.Logging.Log get_Log()
     Microsoft.VisualBasic.ApplicationServices.AssemblyInfo get_Info()
     System.Globalization.CultureInfo get_Culture()
     System.Globalization.CultureInfo get_UICulture()
     Void ChangeCulture(System.String)
     Void ChangeUICulture(System.String)
     System.String ToString()
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.Type GetType()
 Properties:
     Microsoft.VisualBasic.Logging.Log Log
     Microsoft.VisualBasic.ApplicationServices.AssemblyInfo Info
     System.Globalization.CultureInfo Culture
     System.Globalization.CultureInfo UICulture
 Fields:
 Events:
Type name: People.My.MyComputer
 Constructors:
     Void .ctor()
 Methods:
     Microsoft.VisualBasic.Devices.Audio get_Audio()
     Microsoft.VisualBasic.MyServices.ClipboardProxy get_Clipboard()
     Microsoft.VisualBasic.Devices.Ports get_Ports()
     Microsoft.VisualBasic.Devices.Mouse get_Mouse()
     Microsoft.VisualBasic.Devices.Keyboard get_Keyboard()
     System.Windows.Forms.Screen get_Screen()
     Microsoft.VisualBasic.Devices.Clock get_Clock()
     Microsoft.VisualBasic.MyServices.FileSystemProxy get_FileSystem()
     Microsoft.VisualBasic.Devices.ComputerInfo get_Info()
     Microsoft.VisualBasic.Devices.Network get_Network()
     System.String get_Name()
     Microsoft.VisualBasic.MyServices.RegistryProxy get_Registry()
     System.String ToString()
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.Type GetType()
 Properties:
     Microsoft.VisualBasic.Devices.Audio Audio
     Microsoft.VisualBasic.MyServices.ClipboardProxy Clipboard
     Microsoft.VisualBasic.Devices.Ports Ports
     Microsoft.VisualBasic.Devices.Mouse Mouse
     Microsoft.VisualBasic.Devices.Keyboard Keyboard
     System.Windows.Forms.Screen Screen
     Microsoft.VisualBasic.Devices.Clock Clock
     Microsoft.VisualBasic.MyServices.FileSystemProxy FileSystem
     Microsoft.VisualBasic.Devices.ComputerInfo Info
     Microsoft.VisualBasic.Devices.Network Network
     System.String Name
     Microsoft.VisualBasic.MyServices.RegistryProxy Registry
 Fields:
 Events:
Type name: People.My.MyProject
 Constructors:
 Methods:
     System.String ToString()
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.Type GetType()
 Properties:
 Fields:
 Events:
Type name: People.My.MyProject+MyWebServices
 Constructors:
     Void .ctor()
 Methods:
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.String ToString()
     System.Type GetType()
 Properties:
 Fields:
 Events:
Type name: People.Genders
 Constructors:
 Methods:
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.String ToString()
     System.String ToString(System.String, System.IFormatProvider)
     Int32 CompareTo(System.Object)
     System.String ToString(System.String)
     System.String ToString(System.IFormatProvider)
     Boolean HasFlag(System.Enum)
     System.TypeCode GetTypeCode()
     System.Type GetType()
 Properties:
 Fields:
     Int32 value__
     People.Genders Male
     People.Genders Female
 Events:
Type name: People.IPerson
 Constructors:
 Methods:
     System.String get_FirstName()
     Void set_FirstName(System.String)
     System.String get_LastName()
     Void set_LastName(System.String)
     Int32 get_Age()
     Void set_Age(Int32)
     People.Genders get_Gender()
     Void set_Gender(People.Genders)
     System.String BuildFullName()
     Void add_InstanceCreated(InstanceCreatedEventHandler)
     Void remove_InstanceCreated(InstanceCreatedEventHandler)
 Properties:
     System.String FirstName
     System.String LastName
     Int32 Age
     People.Genders Gender
 Fields:
 Events:
     InstanceCreatedEventHandler InstanceCreated
Type name: People.Person
 Constructors:
     Void .ctor()
 Methods:
     System.String get_FirstName()
     Void set_FirstName(System.String)
     People.Genders get_Gender()
     Void set_Gender(People.Genders)
     System.String get_LastName()
     Void set_LastName(System.String)
     Int32 get_Age()
     Void set_Age(Int32)
     Void add_InstanceCreated(InstanceCreatedEventHandler)
     Void remove_InstanceCreated(InstanceCreatedEventHandler)
     System.String BuildFullName()
     System.String ToString()
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.Type GetType()
 Properties:
     System.String FirstName
     People.Genders Gender
     System.String LastName
     Int32 Age
 Fields:
 Events:
     InstanceCreatedEventHandler InstanceCreated
Type name: People.IPerson+InstanceCreatedEventHandler
 Constructors:
     Void .ctor(System.Object, IntPtr)
 Methods:
     System.IAsyncResult BeginInvoke(System.AsyncCallback, System.Object)
     Void EndInvoke(System.IAsyncResult)
     Void Invoke()
     Void GetObjectData(System.Runtime.Serialization.SerializationInfo,
          System.Runtime.Serialization.StreamingContext)
     Boolean Equals(System.Object)
     System.Delegate[] GetInvocationList()
     Int32 GetHashCode()
     System.Object DynamicInvoke(System.Object[])
     System.Reflection.MethodInfo get_Method()
     System.Object get_Target()
     System.Object Clone()
     System.String ToString()
     System.Type GetType()
 Properties:
     System.Reflection.MethodInfo Method
     System.Object Target
 Fields:
 Events:

Notice also how EventHandler types, generated behind the scenes when you implement a simple event, are inspected and illustrated. Also notice how the members’ signature recalls the Intermediate Language syntax.

Reflecting a Single Type

Reflecting all types within an assembly can be useful, but in most cases you will probably be interested in reflecting a single type. To accomplish this, you need the instance of a System.Type. Then you need to invoke members described in the previous section. For example, imagine you want to inspect members from the Person class. You first get the type instance, and then you can perform reflection as demonstrated by the following code:

Dim myType As Type = (New People.Person).GetType

Console.WriteLine(" Methods:")
For Each method In myType.GetMethods
    Console.WriteLine("     " + method.ToString)
Next

Console.WriteLine(" Properties:")
For Each [property] In myType.GetProperties
    Console.WriteLine("     " + [property].ToString)
Next

Console.WriteLine(" Fields:")
For Each field In myType.GetFields
    Console.WriteLine("     " + field.ToString)
Next

Console.WriteLine(" Events:")
For Each [event] In myType.GetEvents
    Console.WriteLine("     " + [event].ToString)
Next

The preceding code produces the following result:

Methods:
     System.String get_FirstName()
     Void set_FirstName(System.String)
     People.Genders get_Gender()
     Void set_Gender(People.Genders)
     System.String get_LastName()
     Void set_LastName(System.String)
     Int32 get_Age()
     Void set_Age(Int32)
     Void add_InstanceCreated(InstanceCreatedEventHandler)
     Void remove_InstanceCreated(InstanceCreatedEventHandler)
     System.String BuildFullName()
     System.String ToString()
     Boolean Equals(System.Object)
     Int32 GetHashCode()
     System.Type GetType()
 Properties:
     System.String FirstName
     People.Genders Gender
     System.String LastName
     Int32 Age
 Fields:
 Events:
     InstanceCreatedEventHandler InstanceCreated

For more details, the MSDN documentation on the System.Reflection namespace and the System.Type class are a good source of information on available members.


Reflection Security Considerations

Reflection is both a key topic and a powerful tool in the .NET developer toolbox. By the way, you had the opportunity to understand how fragile your code is in security terms because, with a few lines of code, anyone can see types and members exposed by the assembly. Because preventing Reflection is not possible, if you want to protect your code, you need to use an obfuscation tool such as Dotfuscator (shipped with Visual Studio 2012) that can add more effective protection.


Invoking Code Dynamically

Reflection also enables you to execute dynamic code, meaning that you can pick up types defined within an assembly, creating instances and invoking types from Visual Basic code without having a reference to that assembly. For example, imagine you want to load the People.dll assembly and create and populate an instance of the Person class, as shown in Listing 46.3.

Listing 46.3. Creating and Running Dynamic Code


Imports System.Reflection
Module DynamicCode

    Sub DynCode()
        Dim asm = Assembly.LoadFrom("People.dll")

        'Gets the type definition
        Dim personType = asm.GetType("People.Person")

        'Gets the LastName property definition
        Dim lastNameProperty As PropertyInfo = personType.
                                               GetProperty("LastName")
        'Gets a reference to the property setter
        Dim lastNamePropSet As MethodInfo = lastNameProperty.
                                            GetSetMethod

        Dim firstNameProperty As PropertyInfo = personType.
                                                GetProperty("FirstName")
        Dim firstNamePropSet As MethodInfo = firstNameProperty.
                                             GetSetMethod

        Dim ageProperty As PropertyInfo = personType.GetProperty("Age")
        Dim agePropSet As MethodInfo = ageProperty.GetSetMethod

        'Creates an instance of the Person class
        Dim newPerson As Object = _
            Activator.CreateInstance(personType)

        'Each method is invoked upon the new type instance
        lastNamePropSet.Invoke(newPerson, New Object() {"Del Sole"})
        firstNamePropSet.Invoke(newPerson, New Object() {"Alessandro"})
        agePropSet.Invoke(newPerson, New Object() {35})

        'Gets the BuildFullName method from the Person class
        Dim buildFullNameMethod = personType.GetMethod("BuildFullName")

        'The method returns String but Invoke returns Object, so
        'a conversion is required
        Dim result As String = CStr(buildFullNameMethod.
                               Invoke(newPerson, Nothing))

        Console.WriteLine(result)
        Console.ReadLine()
    End Sub
End Module


When you have the type instance, you invoke the GetProperty method to get a reference of the desired property. This returns a PropertyInfo object. To set the property value, you need a reference to the setter method that is obtained via the GetSetMethod and that returns a MethodInfo object. (If you also want the ability to get a property value, you need to instead invoke GetGetMethod the same way.) When you have all the properties, you need an instance of the class. This can be obtained by calling the Activator.CreateInstance method, which takes the type instance as the argument.

The System.Activator class contains members for creating code locally or retrieving code from a remote location. Having an instance of the class is required before you set properties because it is against the instance that property setters will be invoked. To actually run the property setter, you call the MethodInfo.Invoke instance method; the first argument is the type instance, and the second argument is an array of items of type Object, each to be used as a property value. In our case, each property in the Person class accepts just one value, so each array can store just one item. Similarly, you can get reference to methods by invoking GetMethod on the type instance, as it happens in Listing 46.3, to get a reference to the Person.BuildFullName method. When you call Invoke to run the method, you can pass Nothing as the second argument if the original method does not require parameters. The code produces the following result:

Del Sole Alessandro, Male of Age: 35

After seeing how you can call dynamic code provided by an existing assembly, let’s now see how to create code at runtime.


Security Note

In many cases, you can also invoke members marked as private or with limited visibility. Although this can seem exciting, be careful. If you invoke a private member but you are not completely sure about its purpose, you expose your code to potentially uncontrollable dangers.


Generating Code at Runtime with Reflection.Emit

The System.Reflection.Emit namespace provides objects for generating assemblies, types, and type members at runtime. You need to perform the following operations sequentially:

1. Create an in-memory assembly within the current application domain with an instance of the AssemblyBuilder class.

2. Create a module for containing types via an instance of the ModuleBuilder class.

3. Create types with instances of the TypeBuilder class.

4. Add members to the TypeBuilder via XBuilder objects, such as MethodBuilder, FieldBuilder, and PropertyBuilder.

5. Save the assembly to disk if required.

The code in Listing 46.4 demonstrates how to dynamically create a simple implementation of the Person class with one property and one method.

Listing 46.4. Generating Code at Runtime


Imports System.Reflection
Imports System.Reflection.Emit

Module CreatingCode

    Sub CreateAssembly()

        'Creates assembly name and properties
        Dim asmName As New AssemblyName("People")
        asmName.Version = New Version("1.0.0")
        asmName.CultureInfo = New Globalization.CultureInfo("en-US")

        'Gets the current application domain
        Dim currentAppDomain As AppDomain = AppDomain.CurrentDomain

        'Creates a new in-memory assembly in the current application domain
        'providing execution and saving capabilities
        Dim asmBuilder As AssemblyBuilder = currentAppDomain.
                                            DefineDynamicAssembly(asmName,
                                            AssemblyBuilderAccess.RunAndSave)

        'Creates a module for containing types
        Dim modBuilder As ModuleBuilder = _
            asmBuilder.DefineDynamicModule("PersonModule",
                                           "People.dll")

        'Creates a type, specifically a Public Class
        Dim tyBuilder As TypeBuilder = _
            modBuilder.DefineType("Person",
                                  TypeAttributes.Public _
                                  Or TypeAttributes.Class)
        'Defines a default empty constructor
        Dim ctorBuilder As ConstructorBuilder = _
            tyBuilder.DefineDefaultConstructor(MethodAttributes.Public)

        'Defines a field for storing a property value
        Dim fldBuilder As FieldBuilder = _
            tyBuilder.DefineField("_lastName",
                                  GetType(String),
                                  FieldAttributes.Private)

        'Defines a property of type String
        Dim propBuilder As PropertyBuilder = _
            tyBuilder.DefineProperty("LastName",
                                     PropertyAttributes.None, GetType(String),
                                     Type.EmptyTypes)

        'Defines a series of attributes for both getter and setter
        Dim propMethodAttributes As MethodAttributes = _
            MethodAttributes.Public Or
            MethodAttributes.SpecialName Or
            MethodAttributes.HideBySig

        'Defines the getter method for the property
        Dim propGetMethod As MethodBuilder = _
            tyBuilder.DefineMethod("get_LastName",
                                   propMethodAttributes,
                                   GetType(String),
                                   Type.EmptyTypes)

        'Generates IL code for returning the field value
        Dim propGetMethodIL As ILGenerator = propGetMethod.GetILGenerator
        propGetMethodIL.Emit(OpCodes.Ldarg_0)
        propGetMethodIL.Emit(OpCodes.Ldfld, fldBuilder)
        propGetMethodIL.Emit(OpCodes.Ret)

        'Defines the setter method for the property
        Dim propSetMethod As MethodBuilder = _
            tyBuilder.DefineMethod("set_LastName",
                                   propMethodAttributes,
                                   GetType(String),
                                   Type.EmptyTypes)

        'Generates the IL code for setting the field value
        Dim propSetMethodIL As ILGenerator = propSetMethod.GetILGenerator
        propSetMethodIL.Emit(OpCodes.Ldarg_0)
        propSetMethodIL.Emit(OpCodes.Ldarg_1)
        propSetMethodIL.Emit(OpCodes.Stfld, fldBuilder)
        propSetMethodIL.Emit(OpCodes.Ret)

        'Assigns getter and setter to the property
        propBuilder.SetGetMethod(propGetMethod)
        propBuilder.SetSetMethod(propSetMethod)

        'Defines a public method that returns String
        Dim methBuilder As MethodBuilder = _
            tyBuilder.DefineMethod("BuildFullName",
                                   MethodAttributes.Public,
                                   GetType(String),
                                   Type.EmptyTypes)

        'Method body cannot be empty, so just return
        Dim methodILGen As ILGenerator = methBuilder.GetILGenerator
        methodILGen.EmitWriteLine("Method implementation needed")
        methodILGen.Emit(OpCodes.Ret)

        'Creates an instance of the type
        Dim pers As Type = tyBuilder.CreateType

        'Enumerates members for demo purposes
        For Each member In pers.GetMembers
            Console.WriteLine("Member name: {0}", member.Name)
        Next

        'Saves the assembly to disk
        asmBuilder.Save("People.dll")
        Console.ReadLine()
    End Sub
End Module


After you create an AssemblyName for assigning assembly properties and get the instance of the current application domain, you use the AppDomain.DefineDynamicAssembly method to generate an in-memory assembly. The method returns an instance of the AssemblyBuilder class and receives the AssemblyName instance and a value from the AssemblyBuilderAccess enumeration that establishes the access level for Reflection. RunAndSave enables you to execute and save the assembly, but you can also limit Reflection with the ReflectionOnly value.

The next step is creating an instance of the ModuleBuilder class that can act as a container of types. This is accomplished by invoking the AssemblyBuilder.DefineDynamicModule method that requires you to specify the module name and the filename. (This one should be the same as for AssemblyName if you want metadata to be merged into a single assembly.) When you have a module, you can put your types into it. For each type, you need to create an instance of the TypeBuilder class, which you accomplish by invoking the ModuleBuilder.DefineType method that receives the type name and qualifiers as arguments. Qualifiers are one or more values from the TypeAttributes enumeration; in the current example, Public and Class values are assigned to the new type to create a new class with public visibility. The TypeBuilder class provides lots of methods for adding members, such as constructors, field, properties, and methods. For constructors, the code demonstrates how to add a public, empty, and default constructor by invoking the TypeBuilder.DefineDefaultConstructor, but you can supply constructor overloads via the DefineConstructor method.

To implement properties, you first need to supply fields. These are implemented via the TypeBuilder.DefineField method that requires three arguments: the field name; the type (retrieved via GetType); and qualifiers, which are determined with values from the FieldAttributes enumeration. Similarly, you implement properties by invoking the TypeBuilder.DefineProperty method, but this is not enough because you also need to explicitly generate the getter and setter methods for each property. These are special methods that require providing some properties defined within the propMethodAttributes variable that takes values from the MethodAttributes enumeration. When you establish method attributes, you create two MethodBuilder instances. Such a class generates each kind of method, including special ones. You just supply the method name, the attributes, the return type, and an array of type parameters. The actual problem is how you implement method bodies. As a general rule, methods implemented via Reflection cannot have an empty method body, so you must provide some Intermediate Language code to populate the method body. This is accomplished by invoking methods from the ILGenerator class that enable injecting IL code to the method. Consider the following snippet, excerpted from Listing 46.4:

'Generates IL code for returning the field value
Dim propGetMethodIL As ILGenerator = propGetMethod.GetILGenerator
propGetMethodIL.Emit(OpCodes.Ldarg_0)
propGetMethodIL.Emit(OpCodes.Ldfld, fldBuilder)
propGetMethodIL.Emit(OpCodes.Ret)

The MethodBuilder.GetILGenerator method returns an instance of the ILGenerator class. Then you invoke the Emit method to execute IL code. In the preceding snippet, the IL code returns the value of the fldBuilder variable, pushes the value onto the stack, and then returns. Actions to execute via the IL are taken via shared fields from the OpCodes class, each related to an IL instruction.


Note on OpCodes

Reflection is powerful, but because you need to know the MS Intermediate Language in detail before implementing dynamic code, and because this would be beyond the scope in this book, you should look at the appropriate MSDN documentation at http://msdn.microsoft.com/en-us/library/8ffc3x75(v=vs.110).aspx.


When you provide the method body for getters and setters, you add them to the related properties via the PropertyBuilder.SetGetMethod and PropertyBuilder.SetSetMethod methods. Similarly, you implement any other method, and the sample code demonstrates this by providing a simple method body that invokes EmitWriteLine. This is a method that sends to the assembly the appropriate IL code for writing a message to the Console window. Finally, you invoke AssemblyBuilder.Save to save the assembly to disk. More than running the code, you can ensure that everything works by inspecting the assembly with a Reflection tool such as Microsoft IL Disassembler. Figure 46.2 shows how the assembly looks if opened with ILDasm, demonstrating the correct result of our work.

Image

Figure 46.2. The assembly created at runtime opened in IL Disassembler.

Typically, you will use code generators instead of Reflection to generate code on-the-fly because in that case you do not need to know about Intermediate Language. After you define your types on-the-fly, you can consume them using the techniques described in the “Invoking Code Dynamically” section.

Late Binding Concepts

Late binding is a particular programming technique that you use to resolve types at runtime and dynamic loading, which is accomplished by assigning objects to variable of type Object. For a better understanding, consider its counterpart, the early binding. This happens at compile time where the compiler checks that argument types utilized to invoke methods match their signatures. An example is the background compiler that provides real-time checks for types used in code, thanks to early binding. On the contrary, late binding requires you to specify the function signatures. Moreover, you must ensure that the code uses the correct types. This means that binding requirements, such as binary files to load or methods to invoke, is long delayed—in many cases until before the method is invoked. Reflection frequently uses late binding because in many cases you work with objects of type Object, and this requires late resolution for invoking appropriate members. The following example, although not related to Reflection, demonstrates how to invoke members from objects declared as Object that are instead of different types, but this is determined late at runtime:

' This code creates an instance of Microsoft Excel and adds a new WorkBook.
' Requires Option Strict Off
Sub LateBindingDemo()
    Dim xlsApp As Object
    Dim xlsBook As Object
    xlsApp = CreateObject("Excel.Application")
    xlsBook = xlsApp.Workbooks.Add
End Sub


Option Strict Off Best Practices

Because in many situations turning Option Strict to Off can be very dangerous, if you need to work with late binding, you should consider moving the code that requires such a technique to a separate code file and just mark this code file with Option Strict Off, instead of setting it Off at the project level.


As you can see, invoking members from Object in late binding is different because the compiler cannot predetermine whether members exist and you don’t have IntelliSense support. But if the actual type defines members that you are attempting to invoke, they will be correctly bound at runtime. Just remember that late binding requires an Option Strict Off directive, and that should be used carefully.

Caller Information

Reflection is very powerful and enables you to retrieve every possible bit of information from an assembly and types defined in the assembly, including at runtime. However, you might want to catch some information, typically to log some activities, which would be difficult to retrieve via Reflection, at least with an easy approach. The .NET Framework 4.5 makes this easier by introducing a set of new attributes called Caller Information. This is the list of new attributes exposed by the System.Runtime.CompilerServices namespace:

CallerFilePath—Returns the name of the file where a specific code block is being executed

CallerLineNumber—Returns the line number for the code that has been executed

CallerMemberName—Returns the name of a member that has received some changes

In the past, you could retrieve the same information with advanced Reflection techniques, which would also require a full trust context and the inclusion of debug symbol files in the assembly. With Caller Information, you can retrieve such information easily and without the full trust and debug symbol restrictions because they are available through types exposed directly by the .NET Framework. To use Caller Information, you must declare methods that receive optional parameters decorated with the attribute that you need. For a better understanding, let’s consider an example. Listing 46.5 demonstrates how to implement a method that writes a log about some information entered by the user.

Listing 46.5. Writing a Log with Caller Information


Imports System.Runtime.CompilerServices
Module Module1

    Sub Main()
        'Wait for the user input
        Console.WriteLine("Type something: ")
        Dim input = Console.ReadLine

        'show what the user entered:
        Console.WriteLine("You entered: {0}", input)

        WriteLog()
        Console.ReadLine()
    End Sub

    'Declare optional arguments. These are decorated
    'with CallerFilePath and CallerLineNumber attributes
    Private Sub WriteLog(<CallerFilePath()> Optional ByVal file As String = Nothing,
                         <CallerLineNumber()> Optional ByVal line As Integer = 0)
        Console.WriteLine("File: {0}", file)
        Console.WriteLine("Line number: {0}", line)
    End Sub

End Module


In this code, the WriteLog method receives optional arguments, decorated with the CallerFilePath (of type String) and the CallerLineNumber (of type Integer) attributes, respectively. Marking these arguments as optional is mandatory. When you invoke the method, you pass no arguments because the runtime will automatically supply the required information. Figure 46.3 demonstrates how the application shows the name of the file in which the code is executed and the line number.

Image

Figure 46.3. Retrieving the filename and line number for the executed code.

In this case, line number 12 is the invocation to WriteLog. This means you will invoke a method with Caller Information definition at the exact point in which you want to track the line number. The last attribute, CallerMemberName, is only used together with the INotifyPropertyChanged interface. As you might know, the INotifyPropertyChanged interface is used in WPF and Silverlight to let the runtime know that some change occurred on the value of a property. CallerMemberName makes it easier to understand which property has changed. Consider the special implementation of the Person class shown in Listing 46.6.

Listing 46.6. Using CallerMemberName to Retrieve Property Changes


Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public Class Person
    Implements INotifyPropertyChanged

    Private _fullName As String
    Public Property FullName As String
        Get
            Return _fullName
        End Get
        Set(value As String)
            Me._fullName = value
            NotifyChange(_fullName, value)
        End Set
    End Property

    Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
      Implements INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyChange(Of T As IEquatable(Of T))(
                                                      ByRef v1 As T, v2 As T,
                                                      <CallerMemberName()>
                                                      Optional prop As String = Nothing)
        If v1 IsNot Nothing AndAlso v1.Equals(v2) Then Return
        If v1 Is Nothing AndAlso v2 Is Nothing Then Return
        v1 = v2
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(prop))
    End Sub
End Class


Inside the Set method of the FullName property definition, the PropertyChanged event is not raised directly. Instead, the code invokes a separate method called NotifyChange that has an optional argument of type String, decorated with the CallerMemberName attribute. Such an argument will be populated at runtime with the name of the property that is being changed. To understand how it works, declare an instance of the Person class like this:

Dim p As New Person With {.FullName = "Alessandro Del Sole"}

Next, place a breakpoint on this line by pressing F9; then run the code. When the debugger encounters the breakpoint, pass the mouse pointer over the prop variable declared as the optional parameter in NotifyChanges. You will see how such a variable contains the name of the property (FullName in the example) that has been changed when the class has been instantiated.

Summary

In this chapter, we covered one of the most important topics in the .NET development, Reflection. You saw what Reflection is and how assemblies are structured. Talking in code terms, you then saw how to interrogate assembly information and how to reflect types to inspect types and type members exposed by an entire assembly or by a single type. Next, dynamically invoking code from an external assembly without the need of having a reference to that assembly was explained. You then learned how to take advantage of the System.Reflection.Emit namespace to create an assembly, types, and members at runtime. Finally, you learned about Caller Information, a new feature in Visual Basic 2012 that enables you to retrieve information such as the name of the file where a code block is executed, the line number, and the name of a property that has been changed, through three new attributes in the .NET Framework 4.5.

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

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