Reflection in the .NET framework
Lesson 1: Understanding Reflection
Code in CLR is packaged within assemblies. The meta data within the assembly contains type information for all classes, structures, delegates and interfaces in assembly. The assembly is a logical container for the different parts the CLR needs to execute code
- Assembly metadata - defines the assembly, e.g. name, version, strong name, culture information
- Type metadata - describes what a type looks like, e.g. namespace, type name, type members and their parameters, etc.
- Code (IL code) - The IL code that will be compiled to machine code and executed.
- Resources - e.g. strings, images file, etc. used by the code.
An assembly does not need to be contained in a single file, e.g. click-once applications are frequently split into separate files so that only the parts required are pulled down.
Modules are containers for types within an individual assembly. It can be a container within a single assembly, or more usually part of a multi-file assembly. Visual Studio does not offer support for multiple modules - must use command line tools.
Examining Assembly
Assembly class supports several methods to create instances of itself. Some such as GetCallingAssembly and GetExecutingAssembly return instance of a particular assembly. Others such as GetAssembly and Load allow access to assemblies not currently loaded.
Once have instance of Assembly can interrogate its properties, e.g.
Assembly a = Assembly.GetExecutingAssembly();
Console.WriteLine(a.FullName);
Console.WriteLine(a.Location);
Assembly class also supports loading assembly to interrogate its information. Normally when loading assembly there is work involved with preparing it for execution, if you do not expect to execute its code then calling ReflectionOnlyLoad results in faster load times - but some of the methods (e.g. CreateInstance) on the assembly returned by this call will be inoperative.
Each assembly contains one or more modules that represent containers for type information.
Assembly a = Assembly.GetExecutingAssembly();
foreach (Module m in a.GetModules())
{
Console.WriteLine(m.Name);
}
Lesson 2: Assembly Attributes
Common Attributes
Typically add assembly attributes to AssemblyInfo file, e.g.
[assebly: AssemblyCompany("Company Name")]
- AssemblyAlgorithm - specifies which hash algorithm to use when reading file hashes in assembly manifest
- AssemblyCompany - name of the company that produced the assembly. Also places name in DLL header
- AssemblyConfiguration - specifies which configuration (e.g. DEBUG, RELEASE) is used for the assembly
- AssemblyCopyright - specify copyright information
- AssemblyCulture - Normally assemblies are culture neutral, but satellite assemblies use this attribute to specify their culture.
- AssemblyDefaultAlias - Simplifies the name of the assembly, used when the name is long or convoluted
- AssemblyDelaySign - Specifies that assembly will be signed after compilation and marked as strongly named assembly
- AssemblyDescription - provides human readable description of assembly
- AssemblyFileVersion - the file version of the assembly, if not supplied the AssemblyVersion attribute is used
- AssemblyFlags - one or more values from the following
- EnableJITCompileOptimizer - JIT optimisation
- EnableJITcompileTracking - JIT compiler tracking
- None - no effects
- PublicKey - assembly public key based on that specified rather than the public key token
- Retargetable - allows assembly to be retargeted to an assembly from a different publisher
- AssemblyInformationalVersion - version used for informational purposes, runtime does not use this for runtime versioning
- AssemblyKeyFile - specifies path to key file used to sign (strongly named) the assembly
- AssemblyTitle - provides a title for the assembly
- AssemblyTrademark - trademark information for the assembly
- AssemblyVersion - specifies version of assembly in
<major><minor><build><revision>
form. Note, build and revision can be replaced with * in which case runtime will automatically fill them - build being a number incremented once a day and revision a randomly generated number.
Getting Assembly Attributes
Main mechanism is GetCustomAttributes method on the Assembly. To get all attributes...
Assembly a = Assembly.GetExecutingAssembly();
foreach (Attribute attr in a.GetCustomAttributes(false))
{
Console.WriteLine(attr.GetType().Name);
}
or a specific attribute...
Assembly a = Assembly.GetExecutingAssembly();
object[] attrs = a.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false))
Console.WriteLine(((AssemblyDescriptionAttribute)attr[0]).Description);
Lesson 3: Reflection Types
Getting Types
Can get type objects in number of ways:
- From Assembly class
- From Module class
- From instances of Object
- via the typeof keyword
The Type class represents a single type. With it you can get information about the type, e.g.
Type t = typeof(String);
Console.WriteLine(t.Namespace);
Console.WriteLine(t.FullName);
Console.WriteLine(t.IsPublic);
Can also call GetCustomAttributes on a type. Differs to call on assembly as the boolean parameter is ignored on assemblies, but with types instruct the runtime as to whether to ignore inherited attributes - a type can be derived from other types, an assembly cannot.
Note the type of an object remains fixed. Even if cast to another or to one of the interfaces it implements, the Name property for its type will still reflect the underlying type it was created as.
Enumerating class members
The type class provides methods for getting different parts of a type, including methods, properties and fields. Each is represented by an appropriately named class, for example the class representing a field is called FieldInfo, that for a property is PropertyInfo. All these classes derive from MemberInfo. For example to find the name for all the properties
foreach (PropertyInfo prop in t.GetProperties())
{
Console.WriteLine(prop.Name);
}
Can also gain access to nested types via GetNestedType or GetNestedTypes methods. Note this method returns a Type object instead of a specialised class.
Can iterate through all parts of a type via GetMembers call. The MemberInfo object returned contains MemberType property that allows the different parts to be differentiated.
foreach (MemberInfo member in t.GetMembers())
{
if (member.MemberType == MemberTypes.Property)
{
PropertyInfo p = (PropertyInfo)member; Console.WriteLine(p.PropertyType.Name);
}
}
Method Body
Iterating through type information provides a view onto the structure of a type. Does not provide view onto the code. This is access via the MethodBody container. This contains local variables and actual IL that are compiled into machine code at runtime. Gain access via the GetMethodBody method on a MethodBase instance (ConstructorInfo or MethodInfo class).
Binding Flags
Controls how members of a type are retrieved via the GetMembers method. The BindingFlags enumeration contains:
- DeclaredOnly - Ignore inherited members
- Default - No binding flag used
- FlattenHierarchy - declared and inherited members returned (also includes protected members)
- IgnoreCase - case insensitive match
- Instance - members that are part of an instance of type (not static) are returned
- NonPublic - protected and internal members
- Public - public members
- Static - static members
Combine these values together and pass into GetMembers override.
Lesson 4: Writing Dynamic Code
Using Dynamic Code
Reflection allows creation of objects dynamically, even from assemblies not referenced ahead of time.
To create objects...
// gather type information...
Assembly assembly = Assembly.LoadFile("mscorlib.dll");
Type hashType = assembly.GetType("System.Collections.Hashtable");
// get constructor
ConstructorInfo ctor = hashType.GetConstructor(Type.EmptyType);
// Now invoke the constructor
object newHash = ctor.Invoke(new object[] {});
Once have instance of object can use reflection to execute its methods
MethodInfo meth = hashType.GetMethod("Add");
meth.Invoke(newHash, new object[] { "Hi", "Hello"});
Invoking static methods
When dynamically calling static methods can skip creation of object..
Type conoleType = typeof(Console);
MethodInfo writeLineMethod = consoleType.GetMethod("WriteLine", new Type[] {typeof(string});
writeLineMethod.Invoke(null, new object[] {"Hello"});
because the above is a static method the first argument to Invoke is null.
Lesson 5: Creating code at runtime
System.Reflection.Emit contains builder classes to construct assemblies, types, methods, etc.
To build code at runtime it has to be encapsulated like any other - create assembly, then module within assembly, then types within the module.
Each builder class derives from the Info class counterpart, so AssemblyBuilder derives from Assembly, MethodBuilder from MethodInfo and TypeBuilder from Type.
Creating Assembly and Module
First ask AppDomain to create a dynamic assembly via the DefineDynamicAssembly method.
AssemblyName tempName = new AssemblyName();
tempName.Name = "MyTempAssembly";
AssemblyBuilder assemBldr = AppDomain.CurentDomain.DefineDynamicAssembly(tempName, AssemblyBuilderAccess.RunAndSave);
The AssemblyBuilderAccess specifies what can be done with the new assembly - ReflectionOnly, Run, Save, RunAndSave.
Having gained an AssemblyBuilder object it is possible to create a MethodBuilder providing it with a name and file name
ModuleBuilder modBldr = assemblyBldr.DefineDynamicModule("MainMod", "MyTempAssembly.dll");
Using the ModuleBuilder it is possible to create dynamic types.
Defining Types
Use the DefineType method on the ModuleBuilder providing a name and TypeAttributes value.
TypeBuilder typeBldr = modBldr.DefineType("MyNewType", TypeAttributes.Public | TypeAttributes.Class);
Overloaded variants of DefineType allow the base class and interfaces to be implemented to be specified.
Creating Members
The TypeBuilder allows any elements of the type to be defined. First thing to do is create the constructor...
ConstructorBuilder ctorBldr =typeBldr.DefineDefaultConstructor(MethodAttributes.Public);
To write code get the ILGenerator object from it and insert IL code, for example if the constructor is going to do nothing but return:
ILGenerator codeGen = ctorBld.GetILGenerator();
codeGen.Emit(OpCodes.Ret);
The Emit call inserts a new line of IL code into the constructor. The OpCodes class exposes each of the operations available in IL as a separate static method.
Persisting to disk
Once written to disk any other process can attempt to load it and treat it exactly like any other assembly.
AssemblyBldr.Save("MyTempAssembly.dll");