Roy Osherove

View Original

Add Plugins to Your App 2: Search dynamically for plugins without Config Files

This article is an extension to my previous article about plugins.

Update:

I've made the downloads for both articles available from my server:

For this article:

http://files.osherove.com/Plugins_Dynamic_src.zip

for the previous article:

http://files.osherove.com/Plugins_demo.zip

http://files.osherove.com/Plugins_src.zip

 

The last article showed how one can create a plug-in architecture for the application. One of the comments I got on that article mentioned that it should be possible to rid the user of the need to register any custom plugins inside the host Application’s config file.

That comment also mentioned how one would go about this. Reading this, I’ve decided to take the challenge and extend my previous demonstration and make the example application search dynamically for plugins within its own directory.

 

The Game Plan

OK. Our main goal here is basically to rid the user of config files. We want to make sure that when our app loads, it can look through the ‘dll’ files in its directory, find the ones that contain types that support the IPlugin interface and instantiate those plugins. 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 .Net framework is System.Reflction. 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, we 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 we 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 Reflection Namespace. In the method above, We call the GetInterfaces() method for each Type that exists in the given file. We then check whether any of the interfaces for that type are an IPlugin interface. If so, it means we can load it into our application, so we put it in an Array List for safe keeping. We can later return to that ArrayList and use Activator.CreateInstance(Type) on those types and thus instantiate any plugins that we’ve found.

Oops! A small Problem

Using this code would definitely work, and would be totally acceptable if it wasn’t for one, small tiny problem. To explain this problem, you’ll need to know about AppDomain. I’ll spare you my whimpy little 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 (Assembly). 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 one you’ve loaded it into an AppDomain. The only way to unload it is to unload the AppDomain itself.

Several implications can be reasoned from this:

1)      Any DLL file that is checked for IPlugin conformance , will , from that moment on, be loaded in our application for the rest of the current AppDomain’s life (i.e. the application terminates)

2)      If there are lots of DLL files to go through this could mean some serious memory overhead for our application

 

So the problem we 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:

·        We’ll create a new AppDomain, and load all the assemblies we are currently checking into that  AppDomain

·        Once we finished checking and found only those types that can be instantiated, we’ll dump the separate AppDomain

·        We’ll then load all the “good” types into our own AppDomain, thus saving ourselves from having to store ‘garbage’ in our application’s memory

 

The way to create a new AppDomain is pretty 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);



 

·        we instantiate as new AppDomain object using a static method on AppDomain. We pass in a user friendly name for this new AppDomain.

·        We create an instance of the PluginFinder class(which holds a method call ‘SearchPath()’) on the AppDomain. To do this we 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.

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

·        We call the ‘SearchPath()’ method on the Proxy to our real PluginLoader class over at the other AppDomain. We get back an ArrayList containing only those Types which conform to the IPlugin Interface.

·        We Unload the other AppDomain, since we have no more use for it.

·        Now we can move on and create those instances of the plugins just like in the previous article, using Activator.

One of the cool things you'll get to see when you load and unload APpDomains is in the Output window of VS.NET (CTRL+ALT+O). You'll see this message:


'Domain71': Loaded 'c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll', No symbols loaded.

'PluginLoader': Loaded 'c:\documents and settings\roy\my documents\visual studio projects\plugins\pluggableapp\bin\debug\pluggableapp.exe', Symbols loaded.

The program '[2408] PluggableApp.exe: PluginLoader' has exited with code 0 (0x0).

 


Important!

Because we use a Proxy when we ‘talk’ between AppDomains, any object that will be instantiated into this proxy (in our 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 a pretty error-prone task. A very helpful tool and is almost 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.

Summery

OK. So using AppDomains is not a walk in the park, but it’s fairly workable once you understand why everything works like it does. It is a must however, if you’re going to need to unload an assembly at run-time.

One of the most frustrating things about this approach for me was that the first time I tested my plugin loader, running without loading and unloading AppDomains seemed to actually take less memory! It took  me some time to realize where the true power lies here: If your application will be re-loading and searching for plugins many times at runtime  - then the results are more acceptable. even though the AppDomain approach initially takes about 1MB more memory, it returns that investment over time (When the GC collects garbage), leaving your memory on pretty-much the same level(Assembly-wise), while the standard approach will increase your memory amount no matter what.

 

For a more in-depth article about AppDomains Loading and Unloading – see the following article on MSDN(Which is where I got most of my material):

AppDomains and Dynamic Loading – by Eric Gunnerson

 

Download the code for this article here.

This article has also been published on MSDNAA along with some other articles you might be interested in.