Introduction
This article is in response to a request I've received, to give an introduction to reflection in .NET as well as some examples of how it can be used.
So what is reflection? Personally, I think of reflection as the use of meta-data which describes our code; or more specifically, describes objects in our code.
Most database systems provide "system tables" hold information about all the objects (tables, views, stored procedures etc. etc.) in the database. As a exercise, run the following on SQL-Server and see what you get back:
SELECT *
FROM sys.objects
In much the same way, the .NET framework provides a series of classes which, collectively, can be thought of as performing much the same function as system tables in a relational database. They provide data about the different objects (classes, structs, enums, delegates etc.) used in our code as well as functionality to perform actions on those objects (e.g.: instantiating objects, invoking methods etc.). Together these classes form the System.Reflection
namespace.
Reflection Fundamentals
In the System.Reflection
namespace, there are two classes which are key to any reflection operations in .NET. They are the Assembly
and Type
classes and you are likely to end up using either or both of these in any reflection code you write
The Assembly
Class
The Assembly
class represents a single .NET assembly. In most .NET solutions, this can be thought of as the equivalent of a single .dll or a single Visual Studio project. The are several ways of obtaining an Assembly
object:
// C#
// Loads the assembly System.Core.dll for execution.
Assembly theAssembly = Assembly.Load("System.Core, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089");
// Loads the assembly System.Core.dll for reflection only.
Assembly theAssembly = Assembly.ReflectionOnlyLoad("System.Core, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089");
// Gets the currently executing assembly.
Assembly theAssembly = Assembly.GetExecutingAssembly();
// Gets the assembly which called the current method.
Assembly theAssembly = Assembly.GetCallingAssembly();
// Gets the entry or startup assembly (i.e.: The assembly for the web app, console app or Winforms app project)
Assembly theAssembly = Assembly.GetEntryAssembly();
' Visual Basic
' Loads the assembly System.Core.dll for execution.
Dim theAssembly As Assembly = Assembly.Load("System.Core, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089")
' Loads the assembly System.Core.dll for reflection only.
Dim theAssembly As Assembly = Assembly.ReflectionOnlyLoad("System.Core, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089")
' Gets the currently executing assembly.
Dim theAssembly As Assembly = Assembly.GetExecutingAssembly()
' Gets the assembly which called the current method.
Dim theAssembly As Assembly = Assembly.GetCallingAssembly()
' Gets the entry or startup assembly (i.e.: The assembly for the web app, console app or Winforms app project)
Dim theAssembly As Assembly = Assembly.GetEntryAssembly()
As you can see, when using the Load()
and ReflectionOnlyLoad()
method we can optionally include the version, culture and public-key token to pinpoint the assembly we are interested in more precisely.
The Type
class
The Type
class represents the actual type declarations (classes, structs, interfaces, enums etc.) in a .NET assembly. There are three common ways to obtain a Type
object for a type:
// C#
// When we have an instance of an object.
String theString = "Foo";
Type theType = theString.GetType();
// When we know the type we want, but don't have an instance of it.
Type theType = typeof(string);
// When we want to get a type from a specific assembly.
Assembly theAssembly = Assembly.Load("mscorlib");
Type theType = theAssembly.GetType("System.String");
' Visual Basic
' When we have an instance of an object.
Dim theString As String = "Foo"
Dim theType As Type = theString.GetType()
' When we know the type we want, but don't have an instance of it.
Dim theType As Type = GetType(String)
' When we want to get a type from a specific assembly.
Dim theAssembly As Assembly = Assembly.Load("mscorlib")
Dim theType As Type = theAssembly.GetType("System.String")
Once we have our Type
object, we then have access to any of its constituent members (properties, methods, constructors etc.) via the methods of the Type
class. The list is quite extensive, so I am not going to go through it here, but you can find the complete reference on MSDN.
Instead, we are going to look at a simple example which, I hope, will act as a good primer for getting into reflection with .NET.
The Reflection Farmyard
The Reflection Farmyard is a one-page web application. The user selects an animal from the drop-down, clicks the 'Make Sound' button and a textual representation of that animal's sound appears on the page. Here is the ASP.NET markup for the page:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>My Farm Yard</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>
My Farm Yard</h1>
<div>
<asp:DropDownList ID="animalDropDown" runat="server" AppendDataBoundItems="true" />
<asp:Button ID="makeSoundButton" runat="server" Text="Make Sound" OnClick="makeSoundButton_Click" />
</div>
<div>
<asp:Label ID="soundLabel" runat="server" ForeColor="Red" />
</div>
</div>
</form>
</body>
</html>
In a separate assembly (class library project), we have a series of classes (one for each animal) each exposing a single MakeSound()
method which returns the animal's sound as a string, for example:
// C#
public class Cat
{
public string MakeSound()
{
return "Meow!";
}
}
' Visual Basic
Public Class Cat
Public Function MakeSound() As String
Return "Meow!"
End Function
End Class
Note that I am deliberately not following good OO practice by not having an abstract Animal
from which my concrete animal classes will inherit. The reason for this will become obvious later on.
Now let's look at the code-behind for the web-page is this is where the interesting reflection stuff happens. Firstly, we want to populate the drop down list with the name of each animal. To do this we are going to use the name of each of the animal classes in our animal assembly. The code for this is as follows:
// C#
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
animalDropDown.DataSource = GetAnimals();
animalDropDown.DataBind();
}
}
private string[] GetAnimals()
{
Assembly assembly = Assembly.ReflectionOnlyLoad("Ovineware.CodeSamples.ReflectionFarmyard.Animals.CSharp");
IEnumerable<Type> animalTypes = assembly.GetTypes().Where(x => x.Namespace == "Ovineware.CodeSamples.ReflectionFarmyard.Animals.CSharp").OrderBy(x => x.Name);
string[] animals = new string [animalTypes.Count()];
for (int i = 0; i < animalTypes.Count(); i++)
{
Type animalType = animalTypes.ElementAt(i);
animals[i] = animalType.Name;
}
return animals;
}
' Visual Basic
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
If Not IsPostBack Then
animalDropDown.DataSource = GetAnimals()
animalDropDown.DataBind()
End If
End Sub
Private Function GetAnimals() As String()
Dim _assembly As Assembly = Assembly.ReflectionOnlyLoad("Ovineware.CodeSamples.ReflectionFarmyard.Animals.VisualBasic")
Dim animalTypes As IEnumerable(Of Type) = _assembly.GetTypes().Where(Function(x) x.Namespace = "Ovineware.CodeSamples.ReflectionFarmyard.Animals.VisualBasic").OrderBy(Function(x) x.Name)
Dim animals(animalTypes.Count() - 1) As String
For i As Integer = 0 To animalTypes.Count() - 1 Step 1
Dim animalType As Type = animalTypes.ElementAt(i)
animals(i) = animalType.Name
Next
Return animals
End Function
Firstly, we we load the assembly which contains our animal classes. At this stage we can do a reflection-only load as we are not actually going to execute any code in this assembly yet. We then call the GetTypes()
method of the Assembly
class to return all the types in the assembly. We use a LINQ query to filter the types from the namespace we are interested in and sort them in alphabetical order. For each of our Type
objects we then call its Name
property to get its name which is put into an array, which in turn is used to populate the drop-down list.
When the user clicks the 'Make Sound' button, we use reflection once again to initialize the correct animal object and call its MakeSound()
method:
// C#
protected void makeSoundButton_Click(object sender, EventArgs e)
{
Assembly assembly = Assembly.Load("Ovineware.CodeSamples.ReflectionFarmyard.Animals.CSharp");
string typeName = String.Format("Ovineware.CodeSamples.ReflectionFarmyard.Animals.CSharp.{0}", animalDropDown.SelectedValue);
Type animalType = assembly.GetType(typeName);
ConstructorInfo constructor = animalType.GetConstructor(Type.EmptyTypes);
object animal = constructor.Invoke(null);
MethodInfo makeSoundMethod = animalType.GetMethod("MakeSound", BindingFlags.Public | BindingFlags.Instance);
string sound = (string)makeSoundMethod.Invoke(animal, null);
soundLabel.Text = sound;
}
' Visual Basic
Protected Sub makeSoundButton_Click(ByVal sender As Object, ByVal e As EventArgs) Handles makeSoundButton.Click
Dim _assembly As Assembly = Assembly.Load("Ovineware.CodeSamples.ReflectionFarmyard.Animals.VisualBasic")
Dim typeName As String = String.Format("Ovineware.CodeSamples.ReflectionFarmyard.Animals.VisualBasic.{0}", animalDropDown.SelectedValue)
Dim animalType As Type = _assembly.GetType(typeName)
Dim constructor As ConstructorInfo = animalType.GetConstructor(Type.EmptyTypes)
Dim animal As Object = constructor.Invoke(Nothing)
Dim makeSoundMethod = animalType.GetMethod("MakeSound", BindingFlags.Public Or BindingFlags.Instance)
Dim sound As String = DirectCast(makeSoundMethod.Invoke(animal, Nothing), String)
soundLabel.Text = sound
End Sub
Here, we load the assembly again but this time for execution. We then call the GetType()
method on our Assembly
object to get the relevant Type
object as specified by the user's selection in the drop-down list. Once we have our Type
object we get its default constructor by calling its GetConstructor()
method. This method takes an array of Type
objects which match the parameter types of the constructor overload we are interested in. In this example, we want the default constructor so we pass in an empty array. The method returns a ConstructorInfo
object.
Next, we instantiate an instance of our animal class by calling the Invoke()
method on the ConstructorInfo
object. This method takes an array of objects which are passed in as parameters to the constructor. As we are invoking the default constructor, we can just pass in a null
reference.
We then need to get the MakeSound()
method of our animal class. We do this by calling the GetMethod()
method on our Type
object. This method has several overloads, but in this instance the one we are using simply takes the name of the method and a combination of BindingFlags
to specify the method, i.e.: we are asking for the method with the name "MakeSound" which is a public, instance method. The GetMethod()
method returns a MethodInfo
object.
Once we have our MethodInfo
object, we invoke it by calling its Invoke()
method. This method takes as parameters the object we want to invoke the method on, in this case the object stored in the animal
variable; and an array of objects which are passed as parameters to the method. As our MakeSound()
method is parameterless, we again pass in a null
reference.
We cast the result as a String
and use it to set the Text
property of our label.
So why didn't I use an Abstract Class?
As I said earlier, I deliberately didn't use an abstract class in my design. If I had, once we had invoked the constructor to get in instance of the object, we could simply cast it to the base class and call the MakeSound()
method at compile time:
// C#
Animal animal = (Animal)constructor.Invoke(null);
string sound = animal.MakeSound();
' Visual Basic
Dim animal As Animal = DirectCast(constructor.Invoke(Nothing), Animal)
Dim sound As String = animal.MakeSound()
However, as this is an article about reflection and I wanted to demonstrate calling a method dynamically at run-time, using an abstract class would have somewhat defeated the object of exercise!
Gotcha: Getting the Correct Type
A common gotcha when using reflection is getting an unexpected Type
object. This is often as a result of the differences between the GetType()
method of System.Object
and the typeof/GetType
functions.
The GetType()
method will always return the concrete type of an object, regardless of the type it is declared as; whereas the typeof/GetType
functions will return the type that has been declared. It is especially easy to get caught out when using reflection with generics.
Consider a scenario where the Cat
class is a sub-class of Animal
and we have a generic method as follows:
// C#
public static void SomeGenericMethod<T>(T obj)
{
// Always the concrete type of obj.
Type v = obj.GetType();
// Varies depending on the type of T, which may be inferred by the compiler.
Type w = typeof(T);
}
' Visual Basic
Public Shared Sub SomeGenericMethod(Of T)(ByVal obj As T)
' Always the concrete type of obj.
Dim v As Type = obj.GetType()
' Varies depending on the type of T, which may be inferred by the compiler.
Dim w As Type = GetType(T)
End Sub
Now take a look at the following examples:
// C#
Cat cat = new Cat();
Animal animal = new Cat();
Type t = cat.GetType(); // t is Cat.
Type u = animal.GetType(); // u is Cat.
SomeGenericMethod(cat); // v is Cat. w is Cat.
SomeGenericMethod(animal); // v is Cat. w is Animal.
SomeGenericMethod<Animal>(cat); // v is Cat. w is Animal.
' Visual Basic
Dim _cat As Cat = New Cat()
Dim _animal As Animal = New Cat()
Dim t As Type = _cat.GetType() ' t is Cat.
Dim u As Type = _animal.GetType() ' u is Cat.
SomeGenericMethod(_cat) ' v is Cat. w is Cat.
SomeGenericMethod(_animal) ' v is Cat. w is Animal.
SomeGenericMethod(Of Animal)(_cat) ' v is Cat. w is Animal.
Summary
That concludes this whistle-stop introduction to reflection. For more information, take a look at the MSDN documentation for the System.Reflection
namespace.
The example source code is available here.
No comments:
Post a Comment