Sunday, March 15, 2009

.Net Reflection Part 2. Loading Assemblies at runtime.

In my first post about Reflection, I gave a basic demo of what can be done using reflection. How you can access metadata about a class such as properties etc. In this post, I'd like to dig a bit deeper into Reflection and show the true power of Reflection and what can be accomplished.

As you may or may not know, Visual Studio has many great addons that can be found across the web. Basically, Microsoft has an API that developers can code against, and then "plugin" their addons into Visual Studio. How is this accomplished? How does it know how to load the code I wrote into Visual Studio? The basic idea behind this is a simple Plugin System that can be accomplished using Reflection. I'll demonstrate the basic premise here to give you a simple understanding of how this all works. This post will be more of a tutorial kind of deal so follow along with me as I go through the steps and I explain them.

Before we do any code, let me explain the basic idea of what we'll do. We're going to create an Interface that we'll call IPluginInterface which will have one simple get property called "Name". All "plugins" will have to implement this interface. At runtime, we will then look in a specific folder for any dll files that have types that implement this interface. If it does, then we know it's a plugin, and we can load it up into our app. May not make sense now, but bear with me. It'll all make sense once we start coding.

OK, fire up Visual Studio and create a Console Application callaed "AssemblyLoadingDemo". Change the default namespace to: SetFocusDemo.Reflection.Demo. You should now see this:

    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using System.Text;
    5 
    6 namespace SetFocusDemo.Reflection.Demo
    7 {
    8     class Program
    9     {
   10         static void Main(string[] args)
   11         {
   12 
   13         }
   14     }
   15 }

OK, for now we won't do anything in this project. Now Click on File -> New -> Project and select "Class Library". Call it "PluginInterface". Get rid of the default Class1.cs, and add a class called "IPluginInterface.cs". Then, add this code to the interface:

namespace SetFocusDemo.Reflection.Contract
{
    /// 
    /// This interface will need to be implemented by any class that wants to be a plugin
    /// in our Demo Plugin project.
    /// 
    public interface IPluginInterface
    {
        string Name { get; }
    }
}

Simple interface, doesn't do much really except it has the Name getter like we talked about. In a real life scenario, you would probably have multiple interfaces with multiple methods and properties, but again this is just a demo, so one is enough. Also, please note that I put this in the "SetFocus.Reflection.Contract" namespace.

OK, now, let's actually create a few "Plugins". Basically they'll be classes that will implement this interface.First, let's create another Project. Click File -> New -> Project and create a Class Library called "Plugin Library". Then, right click on "References" and click "Add Reference". Select the Projects tab on top and click select the "Plugin Interface" assembly. Once that's done, remove the default "Class1.cs" and add a new class called "CustomPlugin1". Then, add this code:

namespace PluginLibrary
{
    using SetFocusDemo.Reflection.Contract;
public class CustomPlugin1 : IPluginInterface
    {
        public string Name
        {
            get { return "Custom Plugin 1"; }
        }
    }
}

Doesn't do all that much, but it implements the IPluginInterface and just returns it's name. Now, add another class to this project, and call it "AnotherPlugin". Then add this code:

namespace PluginLibrary
{
    using SetFocusDemo.Reflection.Contract;
public class AnotherPlugin : IPluginInterface
    {
        public string Name
        {
            get { return "Another Plugin"; }
        }
    }
}

Same thing as before really. Another class that implements the same interface. Now, click on the Build menu and click "Build PluginLibrary". Once that's done, right click on the "Plugin Library" project on the right side in solution explorer and click on "Open Folder in Windows Explorer". Then, go to the Bin -> Debug folder and copy the "PluginLibrary.dll" file. Then, open your MyDocuments folder and paste it in there.

OK, we finally have all the code set up to actually write our Plugin Loader. This is where the Reflection magic will happen. Go back to the first project that we called "AssemlyLoadingDemo". First, add a reference to the "Plugin Interface" project and add the using statement on top: "using SetFocusDemo.Reflection.Contract;" Add a new class and call it "PluginLoader.cs". This class will have one static method that will return an array of all the IPluginInterface objects it found and created. Here's the actual code of the class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SetFocusDemo.Reflection.Contract;
using System.IO;
using System.Reflection;
 
namespace SetFocusDemo.Reflection.Loader
{
    public class PluginLoader
    {
        public static IPluginInterface[] GetPlugins(string directory)
        {
            if (String.IsNullOrEmpty(directory)) { return null; } //sanity check
 
            DirectoryInfo info = new DirectoryInfo(directory);
            if (!info.Exists) { return null; } //make sure directory exists
 
            List<IPluginInterface> plugins = new List<IPluginInterface>();
            foreach (FileInfo file in info.GetFiles("*.dll")) //loop through all dll files in directory
            {
                //using Reflection, load Assembly into memory from disk
                Assembly currentAssembly = Assembly.LoadFile(file.FullName); 
 
                //Type discovery to find the type we're looking for which is IPluginInterface
                foreach (Type type in currentAssembly.GetTypes())
                {
 
                    if(!type.ImplementsInterface(typeof(IPluginInterface)))
                    {
                        continue;
                    }
 
                    //Create instance of class that implements IPluginInterface and cast it to type
                    //IPluginInterface and add it to our list
                    IPluginInterface plugin = (IPluginInterface)Activator.CreateInstance(type);
                    plugins.Add(plugin);
                }
            }
 
            return plugins.ToArray();
        }
    }
}

There's alot of code here, I know and I'll do my best to explain. Before I do that though, two things to note. First, you'll see a using statment on top to System.Reflection. Secondly, you'll see this line of code: type.ImplementsInterface(typeof(IPluginInterface)) but that's an extension method I wrote. (For more on extension methods, see my blog post about them here.) Right click on the project and add a class called "TypeExtensions". Here's the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace SetFocusDemo.Reflection.Loader
{
    public static class TypeExtensions
    {
        public static bool ImplementsInterface(this Type type, Type interfaceType)
        {
            return type.GetInterfaces().Contains(interfaceType);
        }
    }
}

Simple method that checks to see if a given type implements a specific interface.

OK, now we can go back and explain what's going on here.

The GetPlugins method takes a directory name as it's parameter indicating where we want to check for plugins. First we just do some sanity checking to make sure we don't get any Null Reference Exceptions:

     if (String.IsNullOrEmpty(directory)) { return null; } //sanity check
     DirectoryInfo info = new DirectoryInfo(directory);
     if (!info.Exists) { return null; } //make sure directory exists

Now we know our "info" object points to the directory we want to look for Plugins.

List<IPluginInterface> plugins = new List<IPluginInterface>();

Here we just new up a List that will be used to add the plugins found and returned from the method.

foreach (FileInfo file in info.GetFiles("*.dll")) //loop through all dll files in directory
            {
Here we start enumerating all the files in the directory to find dll files. Note: in a real world app, this can be dangerous. You're loading dll's into your app without really knowing that they're safe. In a real world, you'd load them into a seperate app domain, but that's out of the scope of this post.

     //using Reflection, load Assembly into memory from disk
     Assembly currentAssembly = Assembly.LoadFile(file.FullName);

Here we actually load the Assembly from the DLL file found. The Assembly class had many great methods on it which allows you to do all kinds of stuff at runtime. In our case, we'll just be looking through the assembly to find a "Plugin":

 //Type discovery to find the type we're looking for which is IPluginInterface
                foreach (Type type in currentAssembly.GetTypes())
                {
Now, we loop through all the Types found in the Assembly. GetTypes() returns an Array of all the Types found in that assembly. That includes classes, interfaces etc.

      if(!type.ImplementsInterface(typeof(IPluginInterface)))
      {
         continue;
      }

Here we look at the type to see if it implements our interface. The idea here is, if a class wants to be a plugin to our App, it MUST implement our IPluginInterface. If it doesn't it can't be a plugin, in which case we just "continue" and go to the next type found in the Assembly.

            //Create instance of class that implements IPluginInterface and cast it to type
           //IPluginInterface and add it to our list
           IPluginInterface plugin = (IPluginInterface)Activator.CreateInstance(type);
           plugins.Add(plugin);

If we DID find a type that implements our interface, then we actually Create an Instance of that type using Activator.CreateInstance(type). We then cast it to the type of our interface, and add it to our list. Activator.CreateInstance is a really cool method that allows you to dynamically load up objects at runtime.

Finally we clost up and return the List as an array:

return plugins.ToArray();

Ok, we now have all the code to give this a wirl. Go back to your Program.cs and in the main method add this code:

static void Main(string[] args)
        {
            //load plugins from MyDocuments. This is just a demo, in a real scenario, you'd use some
            //specific custom folder
            IPluginInterface[] plugins = PluginLoader.GetPlugins(
                Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
 
            foreach (IPluginInterface plugin in plugins)
            {
                Console.WriteLine("Plugin: \"{0}\" was found and loaded.", plugin.Name);
            }
 
            Console.ReadLine();
        }

Basically, call our static method that loads up the plugins, loop through them and print out the name. Here's the output you should see:

Plugin: "Custom Plugin 1" was found and loaded.

Plugin: "Another Plugin" was found and loaded.

What happened here? Basically, we copied our "Plugins" to our MyDocuments folder. When our PluginLoader went to look for Assemblies, it looked in the MyDocuments folder, and found our PluginLibrary.dll. It then loaded up the assembly and looked through the Types found there. It found "CustomPlugin1" and "AnotherPlugin". Then, it actually created instances of each of those classes and cast them as IPluginInterface. This is key! At design time, we have NO idea what the type we'll be. Any developer coding against our IPluginInterface API can create their own class that implements IPluginInterface. We have no clue what his class will be. What we DO know is that it will be of type IPluginInterface. Therefore, when we create an instance of it using Activator.CreateInstance, and cast it to the only type we know of which is IPluginInterface. Then, we can call the methods on our interface which in our cast is the Property "Name".

This post turned out to be alot longer than I thought. There's alot happening here, so if you have any questions, seriosuly, email me at a.friedman07@gmail.com or post a comment here.

No comments: