|
|
|
Reflection
|
| Reflection is the mechanism of discovering
class information solely at run time. Reflection is a powerful feature in .Net.
Basically it allows your code to look back at classes, objects, methods,
properties and events at run time. .Net has made these features easy to use API
in the System.Relection namespace. |
| You may ask why this would be useful. Have you
ever seen the custom configuration section handler commonly used? Have you ever
wondered why this mechanism works? Well, I'm going to walk through some code
that shows the basics of dynamically creating objects based on a particular
string format used by .Net.
|
|
Type Information Strings
|
| In .Net, Microsoft uses a common pattern for
specifying type in configuration files. The format is "[ClassName],
[AssemblyName]". This allows you to load types that are not in any of the
executing assemblies. The only requirement is that the referenced assemblies be
in the GAC or in one of the probing paths. |
| Why is this string so powerful? It specifies
all the information you need to create an object of the specified type at
runtime. By using a traditional factory pattern of interfaces with classes that
implement the interface, you can change the type of object performing a task by
simply editing your configuration file. No need to add a new reference and edit
your code. By simply adding the assembly in a path your application can find
with it's supporting assemblies and editing a string you can change the
behavior of your application. Now that's power! |
|
The Code
|
| The code to create the objects is actually
incredibly simple. Just think all this power and simplicity also, what a deal. |
| The first part is to split the string into the
class name and assembly name. This is accomplished with these three lines of
code finding the first comma: |
int commaIndex = typeString.IndexOf(",");
string className = typeString.Substring(0, commaIndex).Trim();
string assemblyName = typeString.Substring(commaIndex + 1).Trim(); |
| Now that we have the type and assembly names
we can start to load our type. To do this we use two methods of the Assembly
class, Load and LoadWithPartialName. We first try to load the assembly assuming
we have a full assembly name of AssemblyName, Version=1.0.0.23,
Culture=neutral, PublicKeyToken=null. If the name given is not a full name,
then the Load method may fail. We than try a second time using the
LoadWithPartialName. This will give us two chances to resolve the assembly.
What if we are trying to load an assembly that is already loaded? Nothing, the
loader will check to see if the assembly is already loaded, if it is it gets a
reference to the loaded assembly.The code for this section is as follows:
|
Assembly assembly = null;
try
{
assembly = Assembly.Load(assemblyName);
}
catch
{
try
{
assembly = Assembly.LoadWithPartialName(assemblyName);
}
catch
{
throw new ArgumentException("Can't load assembly " + assemblyName);
}
}
|
| We then simply call GetType on the assembly
passing in our class string as the first argument. The second argument is
whether it should throw an exception if it fails to load the type. I also set
this to false because I perform a test to see if the type is null rather than
relying on exceptions for performance reasons. The third argument is whether to
ignore case. I generally set this to false for performance reasons but this can
fail if someone gets the wrong casing for the type name. |
|
return assembly.GetType(className, false, false);
|
| Now we have the type that we are looking for.
We need to get a constructor to actually perform the instantiation. We can do
this quite simply by calling GetConstructor on our type. To determine the
constructor we want, we need to supply an array of types that match the parameters
for the constructor we want. In this case we want the default constructor so we
create an empty array of type. We use the simplest GetConstructor call and
simply pass in the type array. This will search all the constructors for a
match disregarding all other attributes of the constructor. The code for this
invocation is the following: |
Type[] types = new Type[0];
ConstructorInfo info = targetType.GetConstructor(types);
|
| Now all we have to do is call Invoke on our
constructor info class to instantiate the object. We also need to check for a
null return value which will indicate a problem instantiating the type. The
code for this is the following |
|
object targetObject = info.Invoke(null);
|
| Pulling all this code together we have the
following. I have broken this into two methods, one to resolve the type and the
main method which instantiates and returns the object. (For all the VB people
I've included a VB version of the code) |
C#
public sealed class ObjectFactory
{
private ObjectFactory()
{}
public static object Create(string typeName)
{
// resolve the type
Type targetType = ResolveType(typeName);
if(targetType == null)
throw new ArgumentException("Can't load type " + typeName);
// get the default constructor and instantiate
Type[] types = new Type[0];
ConstructorInfo info = targetType.GetConstructor(types);
object targetObject = info.Invoke(null);
if( targetObject == null )
throw new ArgumentException("Can't instantiate type " + typeName);
return targetObject;
}
private static Type ResolveType(string typeString)
{
int commaIndex = typeString.IndexOf(",");
string className = typeString.Substring(0, commaIndex).Trim();
string assemblyName = typeString.Substring(commaIndex + 1).Trim();
// Get the assembly containing the handler
Assembly assembly = null;
try
{
assembly = Assembly.Load(assemblyName);
}
catch
{
try
{
assembly = Assembly.LoadWithPartialName(assemblyName);
}
catch
{
throw new ArgumentException("Can't load assembly " + assemblyName);
}
}
// Get the handler
return assembly.GetType(className, false, false);
}
}
VB.Net
Public NotInheritable Class ObjectFactory
' Private constructor to prevent instantiation
Private Sub New()
End Sub
Public Shared Function Create(ByVal typeName As String) As Object
' resolve the type
Dim targetType As Type = ResolveType(typeName)
If targetType Is Nothing Then
Throw New ArgumentException("Can't load type " + typeName)
End If
' get the default constructor and instantiate
Dim types(-1) As Type
Dim info As ConstructorInfo = targetType.GetConstructor(types)
Dim targetObject As Object = info.Invoke(Nothing)
If targetObject Is Nothing Then
Throw New ArgumentException("Can't instantiate type " + typeName)
End If
Return targetObject
End Function
Private Shared Function ResolveType(ByVal typeString As String) As Type
Dim commaIndex As Int32 = typeString.IndexOf(",")
Dim className As String = typeString.Substring(0, commaIndex).Trim()
Dim assemblyName As String = typeString.Substring(commaIndex + 1).Trim()
' Get the assembly containing the handler
Dim targetAssembly As [Assembly] = Nothing
Try
targetAssembly = [Assembly].Load(assemblyName)
Catch
Try
targetAssembly = [Assembly].LoadWithPartialName(assemblyName)
Catch
Throw New ArgumentException("Can't load assembly " + assemblyName)
End Try
End Try
' Get the type
Return targetAssembly.GetType(className, False, False)
End Function
End Class
|
|
Performance
|
| Reflection is slower than other methods of
creating objects. While the class I outlined here provides a great deal of
flexibility it is not suitable for high volume applications. There are tricks
that can be used such as caching type info to boost the performance. This
approach should be evaluated for suitability in any application. If performance
is a key issue then a different architecture may be called for. |
| Take the demo application for a test drive.
Hopefully this will open your eyes as to the true power of reflection. |