Search Dynamically for Plug-Ins

 

Roy Osherove

December 2003

Summary: Extends the framework for adding plug-in support to your .NET applications to allow you to also search dynamically for plug-ins within your application's own directory. (5 printed pages)

Applies to
   Microsoft® ASP.NET

Download the source code for this article.

Contents

The Game Plan
System.Reflection to the Rescue
A Small Problem
A Helpful Debugging Tool
Conclusion

The Game Plan

First, this article is an extension to my previous article about plug-ins. I encourage you to take a look at that before diving into this article. That said, my main goal in this article is to rid the user of config files. The idea is to make sure that when your application loads, it can look through the .DLL files in its directory, find the ones that contain types supporting the IPlugin interface, and instantiate those plug-ins. No user intervention should be required, other than copying the .DLL file into the application's directory.

System.Reflection to the Rescue

One of the most powerful namespaces in the Microsoft® .NET Framework is System.Reflection. As its name implies, it allows the code to "reflect" upon itself, exposing any properties, members (both public and private), methods, interfaces, inheritance chains—practically anything you wanted to know about Type X but never dared ask.

Using this powerful namespace, you will go over each file, discovering all of the types that reside inside it, and, for each type, find out whether it supports the IPlugin interface. The class you need to use to get all the types out of a .NET assembly is called System.Reflection.Assembly. Here's a simple method that uses this class for exactly what we discussed:

private void TryLoadingPlugin(string path)
{
    Assembly asm= AppDomain.CurrentDomain.Load(path);
    foreach(Type t in asm.GetTypes())
    {
        foreach(Type iface in t.GetInterfaces())
       {
            if(iface.Equals(typeof(IPlugin)))
            {
                AddToGoodTypesCollection(t);
                break; 
            }
       }
    }
}

As you can see, it's a fairly easy process to retrieve lots of information about any given assembly file, simply by using the System.Reflection namespace. In the method above, you call the GetInterfaces() method for each Type that exists in the given file. You then check whether any of the interfaces for that type are an IPlugin interface. If so, it means you can load it into your application; put it in an Array List for safe keeping. You can later return to that Array List and use Activator.CreateInstance(Type) on those types and thus instantiate any plug-ins that you've found.

A Small Problem

Using this code would definitely work, and would be acceptable if it wasn't for one small problem. To explain this problem, you'll need to know about AppDomain. I'll spare you my explanation about what AppDomains are. Instead, I'll quote the documentation on this one:

Application domains, which are represented by AppDomain objects, provide isolation, unloading, and security boundaries for executing managed code.

Multiple application domains can run in a single process; however, there is not a one-to-one correlation between application domains and threads. Several threads can belong to a single application domain, and while a given thread is not confined to a single application domain, at any given time, a thread executes in a single application domain.

Application domains are created using the CreateDomain method. AppDomain instances are used to load and execute assemblies. When an AppDomain is no longer in use, it can be unloaded.

I'll add the following, which is relevant to our issue: Any assembly that is loaded in the application is loaded by default into the application's AppDomain. That's not a bad thing in itself, until you consider the following fact: You can't directly unload an assembly once you've loaded it into an AppDomain. The only way to unload it is to unload the AppDomain itself.

A couple of implications can be drawn from this:

  1. Any .DLL file that is checked for IPlugin conformance, will, from that moment on, be loaded in your application for the rest of the current AppDomain's life (that is, the application terminates).
  2. If there are lots of .DLL files to go through, this could mean some serious memory overhead for your application.

So the problem you face now is how to go through all the files in the directory, load assemblies, but still be able to unload them. The solution to this is pretty much what you'd expect:

  1. You'll create a new AppDomain, and load all the assemblies you are currently checking into that AppDomain.

  2. Once you have finished checking and have found only those types that can be instantiated, you'll dump the separate AppDomain.

  3. You'll then load all the "good" types into our own AppDomain, thus saving yourself from having to store garbage in your application's memory.

    The way to create a new AppDomain is simple:

        AppDomain domain = AppDomain.CreateDomain("PluginLoader");
        PluginFinder finder = (PluginFinder)domain.CreateInstanceFromAndUnwrap(
            Application.ExecutablePath,"Royo.PluggableApp.PluginFinder");
        ArrayList FoundPluginTypes = finder.SearchPath(Environment.CurrentDirectory);
        AppDomain.Unload(domain);
    
  4. You instantiate a new AppDomain object using a static method on AppDomain. You pass in a user-friendly name for this new AppDomain.

  5. You create an instance of the PluginFinder class (which holds a method call SearchPath()) on the AppDomain. To do this you pass in (much like when using Activator) the name of the assembly in which the class resides, and the full name of the class to instantiate.

  6. What you get back from the last operation is actually a Proxy that looks and behaves like your PluginLoader class, but is actually a mediator between your application's AppDomain and the new AppDomain you have just created. From the discussion above, you know that from this moment on, any assemblies loaded by PluginLoader will actually be loaded inside your new AppDomain and not in your application's AppDomain. This means that after this class has done its job, you will be able to unload the new AppDomain, thus getting rid of the garbage memory.

  7. You call the SearchPath() method on the Proxy to your real PluginLoader class over at the other AppDomain. You get back an Array List containing only those Types conforming to the IPlugin Interface.

  8. You unload the other AppDomain, since you have no more use for it.

  9. Now you can move on and create those instances of the plug-ins, just like in my previous article, using the Activator class.

Important!

Because you use a Proxy when you talk between AppDomains, any object that will be instantiated into this proxy (in this case, PluginLoader) will have to be serializable. You must either make PluginLoader inherit from MarshalByRefObject or put the [Serializable] attribute on that class. If you don't, you'll get an exception:

"Additional information: The type Royo.PluggableApp.PluginFinder in Assembly PluggableApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null is not marked as serializable."

A Helpful Debugging Tool

Working with AppDomains and debugging exceptions that can arise from loading and unloading them can be an error-prone task. A helpful tool that is almost entirely undocumented is the fuslogvw.exe tool, or Fusion Log Viewer. Fusion is the name of the loading subsystem. You can tell the tool to log failures. If you get errors while loading assemblies, refresh the view on this tool and you should get a specific log of the exception.

Conclusion

Using AppDomains is not a walk in the park, but it's fairly workable once you understand why everything works the way it does. It is necessary, however, if you're going to need to unload an assembly at run time.

For a more in-depth article about AppDomains loading and unloading, see AppDomains and Dynamic Loading by Eric Gunnerson (which is where I got most of my material—thanks, Eric!)

About the Author

Roy Osherove has spent the past 5+ years developing data-driven applications for various companies in Israel. He's acquired several MCP titles, written a number of articles on various .NET topics (most of which can be found on his weblog), and loves discovering new things everyday. Roy is also the author of the Feedable service and of the free regular expression tool, The Regulator.

© Microsoft Corporation. All rights reserved.