In This Chapter
• Understanding Assemblies’ Metadata
• Getting Assembly Information
• Generating Code at Runtime with Reflection.Emit
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.
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.
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.
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.
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.
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.
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.
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.
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.)
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.
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 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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.