Introduction

Plugins are a powerful tool in a developers arsenal for a number of reasons
which I won't go into here. I mean, if you're reading this, odds are you already
know you want to use them.

The Problem

The pitfalls in a plugin based environment are typically imposed by the
system using them. For example, with distributed applications, plugins are nice
because they allow easy code addition. They're also a fairly tolerant system.
Usually upgrades are done while the application is shut down and file locking
issues are not usually a problem.

Web applications, on the other hand, tend to be more finicky. If you load a
library which is used by frequent visitors to your site, you're going to be hard
pressed to find a time when that file is not locked. The only solution in that
case is to shut IIS down, update the file and bring it back up. In our case this
happens on a weekly (and sometimes semi-weekly) basis, and is not acceptable.

The Solution

The answer is to make a system which can be upgraded on the fly with minimal
potential for error. This requires a couple things:

Libraries can not be directly used

The folder needs to be scanned for updates

Unused files need to be deleted

Libraries need to be indexed

Each of these points needs to addressed. First, however, we need to make an
architectural decision. I decided that I wanted my library to form an abstract
class. I like the idea of having a Broker class which handles all calls
necessary to create the classes being plugged in. So our final result will be an
abstract class with protected static methods for methods the broker will use (so
our broker doesn't need to be instanced) and private static methods for all
other calls.

The parameters I selected can be explained fairly easily. The
PluginClass distinguishes this set of plugins from any other set.
The Class parameter specifies the actual class you want to create.
This can be fully namespaced, but it can be given as just the class name if you
trust that this is unique. The Folders provide a list of (fully
qualified) paths to search through. The last parameter,
PluginTypes, is an array of System.Type objects which
provide the list of types the plugins may be derived from. This just helps our
sanity later on when we need to make sure we don't load the wrong kind of
plugin.

Now lets flesh the method out a bit. We're just going to have it load up our
configuration file (if it exists) and call a couple other routines to populate
it.

publicabstractclass PluginLoader
{
protectedstatic Type FindAssembly(string PluginClass,
string Class, string[] Folders, params Type[] PluginTypes)
{
// Look at each folder specified, but stop if we find the library
foreach (string Folder in Folders)
{
// Load up the file - LoadXmlFile is tolerant of a bad file,
// just in case
System.Xml.XmlDocument PluginLibrary = LoadXmlFile(
System.IO.Path.Combine(Folder, "plugins.xml"));
if (PluginLibrary == null)
// Our file doesn't exist, so create one
PluginLibrary = CreatePluginFile(Folder, PluginClass, PluginTypes);
else// Our file does exist, so make sure it doesn't need updating
PluginLibrary = UpdatePluginFile(Folder, PluginClass, PluginTypes);
// This should never be null, but it may be if the folder is bad or
// something. I probably shouldn't ignore this, but oh well.
if (PluginLibrary != null)
{
// TODO: Check to see if the library is in here.
// If so, load it up and return it
}
}
// We didn't find it, so return a null
returnnull;
}
// This just loads up that xml file and ignores any errors.
// I... uh... left error
// handling out as an exercise for the reader. Yeah, that's it...
privatestatic Xml.XmlDocument LoadXmlFile(string Path)
{
if (System.IO.File.Exists(Path))
{
try
{
Xml.XmlDocument Result = new Xml.XmlDocument();
Result.Load(Path);
return Result;
}
catch
{
returnnull;
}
}
elsereturnnull;
}
}

Everything should be fairly straightforward so far. We have to write the
CreatePluginFile() and UpdatePluginFile() methods and
fill in the TODO section. This covers the requirement to scan the folder for
updates (from the list above). At this point, a discussion of the XML file is
warranted.

I've decided that plugins will be scanned for pertinent classes and this data
will be placed in an xml file called plugins.xml in the folder containing
the libraries. Multiple types of plugins can be indexed in the same file, so
this satisfies the requirement of indexing the library fairly nicely.

As you can see from the code, data is being read from a file called
plugins.xml. Presumably we should write to this same file. For the format
of the file, I decided to use the following:

There's a good amount of data in here. I'll briefly touch on the important
parts. Plugins is our root node. The timestamp here is just for
information purposes. Retired contains plugin elements
marked for deletion. The system should delete these whenever locks on them have
died to conserve server space (trust me, it bites when you unexpectedly run out
of space due to this folder not being cleared out). The active
block are plugins which are currently available. We'll use it's
updated attribute later to determine whether files are new. The
type attribute matches the PluginClass specified
above. This prevents plugins of different types from bumping into each other.

In the plugin element, the interface attribute just
tells us which interface was matched (so we can verify that this should be
included), the name and fullname attributes specify
the class name and class name along with all namespaces respectively. The
contents of the element is the location of the DLL itself. You may notice that
it points to files in the temp directory. We will copy the files there when we
notice new ones.

So we need to load up the library and return the appropriate type. This is
easy enough now that we know where in the xml file this data is. So...

... replace the TODO section above with this little gem and, pending a valid
file, we're all set. The rest of this deals with that "pending". For the
uninitiated, the code above finds the file path for the desired assembly, loads
it up using Reflection, and creates a new Type object accordingly.

Now because this article is getting a little long, here are the
CreatePluginFile() and UpdatePluginFile() methods:

These are pretty straightforward. CreatePluginFile() creates a
new XML file and populates it using AddAssembliesToPluginFile().
UpdatePluginFile() looks to see if files for this
active element. If any files are newer, it moves all
plugin elements into the retired element and tries to
delete them. This covers the "Unused files must be deleted" requirement. If any
of this changed the file, this routine saves it. Now all we have left is
AddAssembliesToPluginFile() which will cover our last requirement
of copying files to a temporary location.

This is unfortunately a little ugly. We loop over all DLL files in the folder
(we really should do all DLL and EXE files, but I can't do everything for you).
We copy the file into our temp folder and get the new path for it. Then we loop
over all the classes in that library. Within that we loop over all interfaces
each class inherits. Within that we loop over all types we want to find. If any
of those match, we consider this type to be valid. Add the necessary data to the
XML file and move on to the next class. Voila, all requirements are satisfied.

Usage

Now that we've got this great little library, we need to be able to put it to
some use. This is a very simple WidgetBroker class which
demonstrates the use of this class (Note that I added an alternate definition of
FindAssembly() which accepts a single folder and passes it in as an
array).

The WidgetBroker finds the Type using the library we just
created, and creates it (assuming it has a default constructor).

Failure Points

Of course, because this needed to fit into a decent sized article, I couldn't
do out a full implementation of this (and because I'm lazy I haven't done one,
so don't ask for it). So there are a couple failure points. One is if a lot of
people are at your site while you update or add your DLLs. This isn't a huge
issue because the system is just kinda tolerant of that. It builds the files on
the fly, uses them, but doesn't bitch if it can't save them. This can be a
problem if there's something really wrong (like your disk is full). These can
all be fixed with judicious use of Reader/Writer locks. I believe though that
the real solution is to have the application do this poll the first time the
broker class is called. At that point it should set up a
System.IO.FileSystemWatcher class to watch and see if any of the
libraries change. Once changed, the individual changes can be stored to the
plugin.xml file and persisted to disk.

This also doesn't copy in assemblies which are used by other assemblies
appropriately. There are a few reasons this is difficult, the primary of which
is that you have to change around the assembly linkage (which would have changed
an "intermediate" article into an "advanced" article). If you copy the
referenced libraries as-is (using the default filename), they end up getting
locked in your temp folder. You can't really change the name of the file to get
around the locking issues like we did before because you have to update your
primary assembly to point to the new files. I decided not to mess with it and
say that your plugin is not allowed to reference external files.

Summary

So we pretty much created this deal. You can add some extra methods that give
you extra pieces of functionality or easier ways to call the library. If you
download the source above you can see a couple that I did along with a spiffy
little application which demos why this technique is useful. We laughed, we
cried, I think we got some code done.

Whatever.

History

Version

Notes

Random Barnyard Animal

1.0

Brand Spankin' New

Chicken

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

I wonder if someone can give me a hand here. I've implemented a pretty standard, simple plugin system.
Plugins implement the IPlugin interface. I load the assembly, and loop through assembly.GetTypes() looking for an IPlugin and then doing Activator.CreateInstance.

Problem: I want to extend the interface. But, if I add a Property (for example) to the interface, plugins compiled with the old interface no longer work: the call to GetTypes throws an exception (Unable to load one or more of the requested types). There are already several plugins that have been distributed and I don't want to break them.

Is there a standard solution to this problem? It seems very inelegant to create a new interface (e.g. IPluginEx) everytime I want to add a functionality.

You're right, it doesn't explicitly unload the assmeblies. It does however deal with the issue of updating assemblies which are currently in use.

The assemblies are not locked, allowing them to be updated. Once updated, subsequent calls to create tht class use the new assembly, while old instances continue to use the old assembly (which is required). Once all uses of the old assembly are destroyed (the assembly is no longer in use), the temporary file is deleted.

agoat wrote:It does however deal with the issue of updating assemblies which are currently in use.

No it does not work like that Enumerating thru the loaded assemblies will show infact that "duplicates" are loaded. Have fun debugging that! Not to mention the fact that the application will in fact have a "memory leak".

agoat wrote:Once all uses of the old assembly are destroyed (the assembly is no longer in use), the temporary file is deleted.

I forgot to point out an important point of the demo. While it's running you can create and copy in new dlls to the plugin folder. The next time that you do anything, it'll recognize the new dll and add it to the list of available plugins.

Also, forgive any issues with the writing style. I don't write articles often enough, so once in a while, things tend to flow in the order of my stream of consciousness.