System.Reflection

System.Reflection – Generic Methods in C# Using Reflection

Posted on Updated on

Generics came on the scene in .NET 2.0 and have been a mainstay ever since! I have developed numerous application frameworks over the years and in every case I have made full use of generics. Very similar to templates in C++, generics allow dynamic type-based operations in a strongly-typed manner and can provide build-time validation. Generics allow us define classes and methods without committing to specific types while we are coding. I am not going to go into a discussion of generics in a general sense since this MSDN resource does a pretty good job of providing a description.

Generic Methods

For our example on to how invoke generic methods via Reflection, we are first going to create a few simple classes whose actual function is a bit nonsensical, but I believe the best way to explain intermediate to advanced concepts is to use simple examples, and thus this is the theme of a large part of my blog :).

  1. To start, using Visual Studio let’s create a new solution that we will call GenericMethodWalkthroughSolution.
  2. Next, let’s add two projects to our solution: a C# Class Library which we will call MyClassLibrary and a C# Console Application which we will call MyConsoleApplication.
  3. In the Console application, add a project reference to MyClassLibrary.
  4. Right-click the MyConsoleApplication project and selected “Set as Startup Project”.

When we are done with these four steps, our Solution Explorer should look like this:

Solution Explorer View with both projects and a project reference added
Solution Explorer View with both projects and a project reference added

Next, we will add four classes to our Class Library project. For fun, I am using classes that you may remember from your very first introduction to Object-Oriented Programming (OOP). Don’t laugh though as they are perfect for this discussion!

The classes are outlined below:

  • Mammal.cs – a class that will be the base class for the remaining two classes.
  • Human.cs – a class that will be instantiated to represent a human.
  • Dog.cs – a class that will be instantiated to represent a dog
  • Manager.cs – a class that will contain a generic method that will create an instance of a human or a dog, generically and a couple of methods that create either specifically.

We will just add a couple of properties to each of the first three then a single generic method to the fourth.

Since the Mammal class is our base class, let’s define a couple of properties that are common to all mammals. How about Weight and Age? Sounds good to me.

Our Mammal class looks like this:

public class Mammal
{
    public Int32 Weight
    {
        get;
        set;
    }

    public Int32 Age
    {
        get;
        set;
    }
}

Our Human class has a couple of properties and inherits Mammal:

public class Human : Mammal
{
    public string Name
    {
        get;
        set;
    }

    public String Occupation
    {
        get;
        set;
    }
}

Our Dog class has a couple of properties and inherits Mammal as well:

public class Dog : Mammal
{
    public String Breed
    {
        get;
        set;
    }

    public Boolean IsLongHaired
    {
        get;
        set;
    }
}

Finally, our Manager class has a single generic method called Create that is generic and accepts any type that inherits Mammal. See the simple code below:

public class Manager
{
    public T CreateMammal<T>()
        where T : Mammal, new()
    {
        return new T();
    }

    public Human CreateHuman()
    {
        return new Human();
    }

    public Dog CreateDog()
    {
        return new Dog();
    }
}

These methods are quite simple, so let’s not focus on their simplicity but focus instead on the concept we’re discussing. The CreateHuman() and CreateDog() methods simply return a new instance of each type. The Create method does the same, but generically and we will move ahead with our examples of how to call generic methods via Reflection.

Let’s return to our Console Application’s Program class and write some code in our static Main() method as shown below.

static void Main(string[] args)
{
    //first, let's just call the methods directly
    Manager manager = new Manager();

    //create a Human
    Human firstHuman = manager.CreateHuman();

    //create a Dog
    Dog firstDog = manager.CreateDog();

    //create a human generically
    Human secondHuman = manager.CreateMammal<Human>();

    //create a dog generically
    Dog secondDog = manager.CreateMammal<Dog>();
}

I included this initial code simply to illustrate normal invocation of each of the methods. Now we are going to invoke each of the methods via Reflection in the code block below.

static void Main(string[] args)
{
    Manager manager = new Manager();
    Type managerType = manager.GetType();

    //invoke the CreateHuman method
    Human human = (Human)managerType.GetMethod("CreateHuman").Invoke(manager, null);

    //invoke the CreateDog method
    Dog dog = (Dog)managerType.GetMethod("CreateDog").Invoke(manager, null);

    //invoke the CreateMammal method and create a Human
    MethodInfo createMammalMethod = managerType.GetMethod("CreateMammal");
    MethodInfo genericCreateMammalMethod = createMammalMethod.MakeGenericMethod(new Type[] { typeof(Human) });
    Human genericHuman = (Human)genericCreateMammalMethod.Invoke(manager, null);

    //invoke the CreateMammal method and create a Dog
    MethodInfo createMammalMethodDog = managerType.GetMethod("CreateMammal");
    MethodInfo genericCreateMammalMethodDog = createMammalMethodDog.MakeGenericMethod(new Type[] { typeof(Dog) });
    Dog genericDog = (Dog)genericCreateMammalMethodDog.Invoke(manager, null);
}

Okay, the last generic method invocations require some discussion. Let’s break out the first generic invocation of the CreateMammal method in the code block below and insert some commentary to provide an explanation.

//invoke the CreateMammal method and create a Human
//As with the other non-generic, Reflection-based invocations in the first two
//methods, we first have to get a reference to the MethodInfo object for the CreateMammal method.
MethodInfo createMammalMethod = managerType.GetMethod("CreateMammal");

//When invoking a generic method, we have to call the MakeGenericMethod method to tell the runtime
//what generic arguments are involved. In this case, we have one argument whose type is Human.
//Therefore, we create a new MethodInfo object that contains the substituted types for the generic argument(s).
MethodInfo genericCreateMammalMethod = createMammalMethod.MakeGenericMethod(new Type[] { typeof(Human) });

//Instead of using the first MethodInfo object we actually invoke the second (generic) one.
Human genericHuman = (Human)genericCreateMammalMethod.Invoke(manager, null);

So the important point here is this: to invoke a generic method via Reflection, you must call the MakeGenericMethod method of the MethodInfo object that holds a reference to the method prior to substitution of the generic arguments. Remember that a generic method may have more than one generic type, so therefore the MakeGenericMethod method accepts an object array that is the collection of generic types.

Now, if we don’t know the generic types at runtime like we did in this example, we can get them through the same MethodInfo object!  See the code block below:

MethodInfo method = managerType.GetMethod("CreateMammal");
if (method.IsGenericMethod == true)
{
    Type[] genericArguments = method.GetGenericArguments();
}

If we place a breakpoint on the fourth line of this block and add a Watch to the genericArguments object we can see for ourselves that the generic type is a type that has a BaseType of Mammal. See below:

Generic Argument Display in Watch Window
Generic Argument Display in Watch Window

 

Related Posts

System.Reflection – Working with the Assembly Class

System.Reflection – Working with the Type Class

System.Reflection – Working with the Assembly Class

Posted on Updated on

The objects within the System.Reflection namespace of the .NET Framework provide us a means to get assembly and type information at runtime. Reflection is useful in situations where you need to introspect assemblies and their types, either to gain information or to make use of their functionality dynamically without knowing its specifics in advance. Sounds pretty cool, right? Well it is!

This article will walk you through the basics of Reflection and the Assembly class and will be followed by additional articles that dive much more deeply into the objects within the System.Reflection namespace.

The System.Reflection.Assembly Class

The Assembly class is the primary class within the System.Reflection namespace. This will be our starting point as we take a look at Reflection.

First, we will create a simple Visual Studio solution that we will call ‘ReflectionWalkthroughSolution’.

In this solution, we will add a C# Class Library project which we will call ‘MyClassLibrary’.

Next, we will add a Console Application which we will call ‘MyConsoleApplication’. These names are nonsensical, but this solution is merely for demonstration purposes :).

So, after doing these things, our Solution Explorer should look like this:

.NET Reflection - Solution Explorer Getting Started

MyConsoleApplication is bolded because I have set it as the startup project. This will allow us to hit F5 to debug the app and be able to use the console application as our entry point.

Next, expand MyConsoleApplication and Add a Reference to the MyClassLibrary project. Once this is done we are ready to start writing a little code.

Assembly.GetExecutingAssembly() Method

The first thing we will look at is how to get a reference to the assembly that is currently executing. To do this we will add some simple code in the Main() method of our console application as shown below. After writing this code, place a breakpoint on the line, then hover the thisAssembly object and take a look at the properties of the assembly and their values.

static void Main(string[] args)
{
    //get the currently executing assembly
    Assembly thisAssembly = Assembly.GetExecutingAssembly();

}

With the code running, when the breakpoint is hit, we can right-click and Add a Watch to the thisAssembly object. When we hit F10 to step over the line, the Watch window is populated with information pertaining to the ‘thisAssembly’ object. See the illustration below.

Hint: Click the image to view it full-size.

.NET Reflection - Assembly.GetExecutingAssembly() Watch Window
Properties of the ‘thisAssembly’ Assembly object

 

Let’s discuss some of the properties that are shown in the Watch window above.

The CodeBase property returns the full path at which the assembly resides. In other words, the execution location for the assembly, including the name of the assembly.dll file itself.

The CustomAttributes property is an IEnumerable<CustomAttributeData> type that displays all of the custom attributes that exist at the assembly level. If you take a look you will see that many of these come directly from the AssemblyInfo.cs file that is found under the Properties folder of the project.

Assembly.CustomAttributes Illustration
The Assembly.CustomAttributes property expanded.

We will dive into custom attributes in a later post. Attributes can be applied to assemblies, classes, properties, methods, and more and through Reflection can be retrieved and read using the appropriate objects. These attributes exist at the assembly level. For now, just be aware that these attributes can be retrieved in this way.

The DefinedTypes property is an IEnumerable<TypeInfo> type that displays all of the types (classes) defined within the assembly. In our simple little console application we only have one defined type. See below.

The Assembly.DefinedTypes property expanded
The Assembly.DefinedTypes property expanded

Looking ahead to later posts, once you have a reference to an Assembly, knowing the types defined in the assembly allow you to dive into the assembly and then into each type as needed.

The FullName property is a string value that displays the full name of the assembly. This is important when discussing a topic such as strong naming.

The Assembly.FullName property
The Assembly.FullName property

There are other properties of the Assembly object that are noteworthy for this introduction. They are the GlobalAssemblyCache property which tells you whether or not the assembly is GAC’ed, the ImageRuntimeVersion which tells you the full version of the .NET Framework against which the assembly is compiled, and the MainfestModule which gives specific information about the assembly itself.

Getting Assemblies loaded in the Current AppDomain (AppDomain.Current.GetAssemblies() Method)

Now that we’ve taken a quick look at the properties of an Assembly and how to get a reference to the currently executing assembly, let’s branch out a little and retrieve all of the assemblies referenced and loaded into the current AppDomain. An AppDomain is an isolated environment in which an application executes. The AppDomain class belongs to the System namespace. To retrieve the AppDomain for the currently executing application, we will the AppDomain.Current property.

If we return to the Main() method of the Program class of our little console application and remove the code we wrote originally and replace it with this code:

static void Main(string[] args)
{
    Assembly thisAssembly = Assembly.GetExecutingAssembly();

    //Get assemblies loaded in the current AppDomain
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

    //create a StringBuilder to hold assembly information
    StringBuilder sb = new StringBuilder();

    //iterate through the array and write info for each assembly
    foreach (Assembly assembly in assemblies)
    {
        //let's create some rudimentary formatted output to show property values
        //for each assembly
        sb.AppendLine("================================================================");
        sb.AppendLine(String.Format("Assembly: {0}", assembly.FullName));
        sb.AppendLine("================================================================");

        sb.AppendLine(String.Format("CodeBase: {0}", assembly.CodeBase));
        sb.AppendLine(String.Format("Location: {0}", assembly.Location));
        sb.AppendLine(String.Format("Number of Types: {0}", assembly.DefinedTypes.Count().ToString()));
        sb.AppendLine(String.Format("Number of Custom Attributes:  {0}", assembly.CustomAttributes.Count().ToString()));
        sb.AppendLine(String.Format(".NET Runtime Version: {0}", assembly.ImageRuntimeVersion));

        //you can add more stuff here to see more properties...
    }

    string output = sb.ToString();
}

When we view the formatted output from our StringBuilder, it should look something like this:

================================================================
Assembly: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.NET/Framework/v4.0.30319/mscorlib.dll
Location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
Number of Types: 3029
Number of Custom Attributes:  36
.NET Runtime Version: v4.0.30319
================================================================
Assembly: Microsoft.VisualStudio.HostingProcess.Utilities, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Windows/assembly/GAC_MSIL/Microsoft.VisualStudio.HostingProcess.Utilities/11.0.0.0__b03f5f7f11d50a3a/Microsoft.VisualStudio.HostingProcess.Utilities.dll
Location: C:\Windows\assembly\GAC_MSIL\Microsoft.VisualStudio.HostingProcess.Utilities\11.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.HostingProcess.Utilities.dll
Number of Types: 12
Number of Custom Attributes:  17
.NET Runtime Version: v2.0.50727
================================================================
Assembly: System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Windows.Forms/v4.0_4.0.0.0__b77a5c561934e089/System.Windows.Forms.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll
Number of Types: 2273
Number of Custom Attributes:  27
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Drawing/v4.0_4.0.0.0__b03f5f7f11d50a3a/System.Drawing.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll
Number of Types: 303
Number of Custom Attributes:  25
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System/v4.0_4.0.0.0__b77a5c561934e089/System.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
Number of Types: 2278
Number of Custom Attributes:  29
.NET Runtime Version: v4.0.30319
================================================================
Assembly: Microsoft.VisualStudio.HostingProcess.Utilities.Sync, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Windows/assembly/GAC_MSIL/Microsoft.VisualStudio.HostingProcess.Utilities.Sync/11.0.0.0__b03f5f7f11d50a3a/Microsoft.VisualStudio.HostingProcess.Utilities.Sync.dll
Location: C:\Windows\assembly\GAC_MSIL\Microsoft.VisualStudio.HostingProcess.Utilities.Sync\11.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.HostingProcess.Utilities.Sync.dll
Number of Types: 5
Number of Custom Attributes:  18
.NET Runtime Version: v2.0.50727
================================================================
Assembly: Microsoft.VisualStudio.Debugger.Runtime, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Windows/assembly/GAC_MSIL/Microsoft.VisualStudio.Debugger.Runtime/11.0.0.0__b03f5f7f11d50a3a/Microsoft.VisualStudio.Debugger.Runtime.dll
Location: C:\Windows\assembly\GAC_MSIL\Microsoft.VisualStudio.Debugger.Runtime\11.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.Debugger.Runtime.dll
Number of Types: 10
Number of Custom Attributes:  17
.NET Runtime Version: v2.0.50727
================================================================
Assembly: vshost32, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Temp/ReflectionWalkthroughSolution/MyConsoleApplication/bin/Debug/MyConsoleApplication.vshost.exe
Location: C:\Temp\ReflectionWalkthroughSolution\MyConsoleApplication\bin\Debug\MyConsoleApplication.vshost.exe
Number of Types: 1
Number of Custom Attributes:  19
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Core/v4.0_4.0.0.0__b77a5c561934e089/System.Core.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Number of Types: 929
Number of Custom Attributes:  25
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Xml.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Xml.Linq/v4.0_4.0.0.0__b77a5c561934e089/System.Xml.Linq.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml.Linq\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.Linq.dll
Number of Types: 86
Number of Custom Attributes:  22
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Data.DataSetExtensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Data.DataSetExtensions/v4.0_4.0.0.0__b77a5c561934e089/System.Data.DataSetExtensions.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Data.DataSetExtensions\v4.0_4.0.0.0__b77a5c561934e089\System.Data.DataSetExtensions.dll
Number of Types: 25
Number of Custom Attributes:  24
.NET Runtime Version: v4.0.30319
================================================================
Assembly: Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/Microsoft.CSharp/v4.0_4.0.0.0__b03f5f7f11d50a3a/Microsoft.CSharp.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\Microsoft.CSharp\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.CSharp.dll
Number of Types: 316
Number of Custom Attributes:  23
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_32/System.Data/v4.0_4.0.0.0__b77a5c561934e089/System.Data.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_32\System.Data\v4.0_4.0.0.0__b77a5c561934e089\System.Data.dll
Number of Types: 1049
Number of Custom Attributes:  21
.NET Runtime Version: v4.0.30319
================================================================
Assembly: System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
================================================================
CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_MSIL/System.Xml/v4.0_4.0.0.0__b77a5c561934e089/System.Xml.dll
Location: C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
Number of Types: 1229
Number of Custom Attributes:  25
.NET Runtime Version: v4.0.30319
================================================================
Assembly: MyConsoleApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
================================================================
CodeBase: file:///C:/Temp/ReflectionWalkthroughSolution/MyConsoleApplication/bin/Debug/MyConsoleApplication.EXE
Location: C:\Temp\ReflectionWalkthroughSolution\MyConsoleApplication\bin\Debug\MyConsoleApplication.exe
Number of Types: 1
Number of Custom Attributes:  14
.NET Runtime Version: v4.0.30319

Pretty simple, right? Now let’s dig into the Assembly a little deeper.

Getting Types within an Assembly (Assembly.GetTypes() Method)

Every Assembly can contain one of more Types. We can retrieve all of these types via the Assembly.GetTypes() method. To try this out, we replace the code in our console application’s Program class’s Main() method with this code:

static void Main(string[] args)
{
    Assembly thisAssembly = Assembly.GetExecutingAssembly();

    //get all of the Types defined in this Assembly
    Type[] types = thisAssembly.GetTypes();

    //robust code always checks for null FIRST
    if (types != null && types.Length > 0)
    {
        //we'll create a StringBuilder for our formatted output
        StringBuilder sb = new StringBuilder();

        //iterate through the Type[] array
        foreach (Type type in types)
        {
            sb.AppendLine("===============================================================");
            sb.AppendLine(String.Format("Type Name: {0}", type.Name));
            sb.AppendLine("===============================================================");

            sb.AppendLine(String.Format("Type FullName: {0}", type.FullName));
            sb.AppendLine(String.Format("Namespace: {0}", type.Namespace));

            sb.AppendLine(String.Format("Is it a Class?: {0}", type.IsClass.ToString()));
            sb.AppendLine(String.Format("Is it an Interface?: {0}", type.IsInterface.ToString()));
            sb.AppendLine(String.Format("Is it Generic?: {0}", type.IsGenericType.ToString()));
            sb.AppendLine(String.Format("Is it Public?: {0}", type.IsPublic.ToString()));
            sb.AppendLine(String.Format("Is it Sealed?: {0}", type.IsSealed.ToString()));

            sb.AppendLine(String.Format("Qualified Name: {0}", type.AssemblyQualifiedName));

            if (type.BaseType != null && !String.IsNullOrEmpty(type.BaseType.Name))
            {
                sb.AppendLine(String.Format("Base Type: {0}", type.BaseType.Name));
            }

            //there are many, many more properties that an be shown...
        }

        string output = sb.ToString();
    }
}

In our console application, there is only one type, the Program class. When we execute the code above we see information for that single type.

===============================================================
Type Name: Program
===============================================================
Type FullName: MyConsoleApplication.Program
Namespace: MyConsoleApplication
Is it a Class?: True
Is it an Interface?: False
Is it Generic?: False
Is it Public?: False
Is it Sealed?: False
Qualified Name: MyConsoleApplication.Program, MyConsoleApplication, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Base Type: Object

Now that we’ve moved down a level and looked at Types within an assembly, let’s move down one more level.

The next post discusses the System.Reflection.Type class and its properties and methods.