Plugin System in C# and .NET

Posted by Tom on 2010-11-23 20:10

I scrawled about Oh Teh Noes last month and said I'd post more, only to get distracted by fractals (it's a long story) like the technical butterfly that I am, but I return to you here to deliver on my promise. So let's talk about Oh Teh Noes' simple-enough-to-work plugin system.

To recap: Oh Teh Noes is a host program for a set of plugins that perform simple checks on the local machine. These checks are called tasks, and each plugin takes the form of a .NET assembly containing one or more tasks. Tasks all inherit from an abstract base class called, imaginatively enough, Task. This is not a direct copy of the code used in Oh Teh Noes - I've stripped out some irrelevancies.

public abstract class Task
{
    public abstract void Run();
}

A (slightly simplified) task to check the remaining space left on the machine's physical drives would look like this:

class DiskSpaceTask : Task
{
    public override void Run()
    {
        foreach (DriveInfo drive in DriveInfo.GetDrives())
        {
            if (drive.DriveType == DriveType.Fixed)
            {
                if (drive.AvailableFreeSpace < 1024 * 1024 * 1024)
                {
                    // Less than a GB remains! Alert the authorities!
                }
            }
        }
    }
}

And now, for a brief asside . . .

Best Bad Solution Available

At this point what I want to do is define a property in the abstract class that must be overridden in the derived classes, but is part of the class, not the objects. What I need is an abstract const, which is a real shame as they don't exist. Or an abstract static property, which doesn't exist either. Hmmm.

Using an attribute here is a pretty clear abuse of the concept. An attribute should be a class agnostic property, but here we only want to be able to apply is to classes which inherit from Task. We can check that at runtime but it's hardly ideal.

The other alternative is to have a static property on the Task class which is also defined in concrete Task classes and then explicitly 'overriden' using the new keyword. Again, no compile-time cover and you need to know about it in advance. Alternatively have the property in base class throw an exception, which is a pretty serious runtime middle finger. And in using properties we will have to instantiate an object based on the Type, simply to get the value of the property when we're enumerating.

Plan D is an abstract property. And it's really not a property of an object. It's a property of a class. And the instantiation stuff still stands.

So attributes it is. Lacking a better alternative it's the lesser of evils. If anyone has any alternatives please let me know, as frankly this kind of thing sets my teeth on edge, but my pragmatic side just about outweights my semantic purity OCD side.

What was I on about?

Oh that's right. Plugins.

The Already-Maligned Attribute

So now I've grudgingly decided to go the attribute route we'd better bite the bullet and hop to it.

public class TaskAttribute : Attribute
{
    // A friendly name for the task
    public string TypeName
    {
        get;
        set;
    }
 
    public TaskAttribute(string typeName)
    {
        TypeName = typeName;
    }
}

We'll use this attribute to decorate the Tasks and allow our host program to fetch meaningful names for them. In the future it will also be on-hand in case we ever need somewhere to stuff more class-level metadata.

Plugin Discovery

When Oh Teh Noes is run it collects a list of all of the DLL files in the program's plugin subdirectory. It then gets a list of types from each of the assemblies and iterates through those, looking for classes which are subclasses of our Task class and are decorated with TaskAttribute. When we've found one we add the class' Type to a dictionary, using the TaskName of the TaskAttribute as a key. The code looks a lot like this:

Dictionary<string, Type> plugins = new Dictionary<string, Type>();
 
DirectoryInfo pluginDirectory = new DirectoryInfo(Path.Combine(
                                    AppDomain.CurrentDomain.SetupInformation.ApplicationBase, 
                                    "plugins"));
                                    
foreach (FileInfo file in pluginDirectory.GetFiles("*.dll"))
{
    Assembly assembly = Assembly.LoadFile(file.FullName);
    foreach (Type type in assembly.GetTypes())
    {
        object[] attributes = type.GetCustomAttributes(typeof(TaskAttribute), true);
        if (type.IsSubclassOf(typeof(Task)) && attributes != null)
        {
            foreach (object attribute in attributes)
            {
                if (attribute is TaskAttribute)
                {
                    TaskAttribute taskAttribute = (TaskAttribute)attributes[0];
                    plugins[taskAttribute.TypeName] = type;
                }
            }
        }
    }
}

Cocked, locked and ready to rock

So we've loaded our assemblies, enumerated their types, loaded the relevant classes (grumble grumble grumble) and are ready to roll. Next step is to create instances of the plugins themselves. We can use the dictionary we built up in the last method to look up Types by their friendly name, and then create them using the Activator class.

Type pluginType = plugins["TaskName"];
object[] args = new object[];
Task task = (Task)Activator.CreateInstance(pluginType, args);

Hopefully that's covered enough to get people started. If you want to see it in situ then please drop by the OhTehNoes GitHub repository.