Building a Plugin Architecture with C#
Written By: Nathan Baker
- 06 Mar 2006 -
Description: I will show how to use C# to create an extensible application using plugins.
Let's write some code
Wow, that took a long time. If you're like me, you probably want to stop futzing with the IDE and start writing code. Well, OK! Switch to the InterfaceDefinitions.cs file and create a new interface in the Interfaces namespace. Here's the code for it:
public interface IPlugin { string Name { get;} string Version { get;} string Author { get;} };
Wow, that was easy! You probably noticed that the name of the interface starts with an I. This is just convention; you can leave it off if you want. I do it because when the Intellisense dialog pops up, it's real easy to just type an I and get a list of all your interfaces. Handy, eh? Anyway, you're probably wondering what this plugin does. The answer: nothing. This plugin just holds information about itself. Let's make a real plugin now:
public interface IMathPlugin: IPlugin { double Add(double a, double b); double Subtract(double a, double b); double Multiply(double a, double b); double Divide(double a, double b); };
If the IMathPlugin:IPlugin syntax looks strange to you, this is called inheritance. In other words, the IMathPlugin interface inherits all the methods from its parent interface. When you inherit from an interface, you agree to implement all the methods in that interface. When an interface inherits from another interface, the methods from the parent interface are effectively added to the child interface. This is just a good way to keep from writing the same thing multiple times!
Since we have this nice inheritance thing set up, let's define another interface:
public interface IStoragePlugin:IPlugin { void Add(object toAdd); void Remove(object toRemove); void EnumStart(); bool EnumNext(); object GetCurr(); };
This is a stub for a very simple storage plugin. You can add items, remove a known item, and walk through the members. Not very fancy, but it serves its purpose: to illustrate that both of our Math and Storage plugins also have the members from the base interface, IPlugin.
Know thyself
Now that we have some plugin interfaces defined, we need a way to know where they are in a plugin file. A plugin file is just a .dll, and it might have several classes in it. You want to make sure you get the right one, and you want to make sure you know what it is. After all, maybe it's a Math plugin, or maybe it's a Storage plugin. Who knows?
First, we're going to define an enumerated type. An enumerated type, or enum, is essentially a list of values. It's easier to demonstrate than explain, so here:
public enum PluginType { Math, Storage, Unknown };
Just put that definition in the Interfaces namespace (along with your interface definitions). A variable of type PluginType can only be one of those three values. Now that we have that, we still need a way to identify plugins in a .dll. Fortunately, .NET is all about metadata. It is possible to define your own metadata attributes which are then available at runtime. If you've ever seen code in square brackets, like this:
[STAThread]
Then you have seen an attribute. This is just a tag you stick on your classes for reference. So what am I waiting for? Let's define our own attribute:
[AttributeUsage(AttributeTargets.Class)] public sealed class PluginAttribute:Attribute { private PluginType _Type; public PluginAttribute(PluginType T) { _Type = T; } public PluginType Type { get { return _Type; } } }
AttributeTargets.Class is an attribute on our attribute (woah) that tells .NET that our attribute is only valid on class definitions, not methods or fields. It contains a single member that says what type of plugin the class is. Now, when we load a .dll, we only have to search for this attribute.
It is possible to create a plugin system without using attributes. However, I think that defining these attributes makes this system a lot cleaner and easier to work with. Note that PluginAttribute is a very generic name, so if you're writing an application called Widget, it might be smart to call your custom attribute WidgetPluginAttribute to avoid name clashes.
Are we done yet?
Nope! There's still one more thing left. In the PluginDemo project, add an empty .cs file named Plugin.cs and add the following code:
namespace PluginDemo { public class Plugin:IPlugin { public static Type GetTypeFromEnum(PluginType T) { switch (T) { case PluginType.Math: return typeof(IMathPlugin); case PluginType.Storage: return typeof(IStoragePlugin); default: return typeof(IPlugin); } } public static string GetTypenameFromEnum(PluginType T) { switch (T) { case PluginType.Math: return "Math"; case PluginType.Storage: return "Storage"; default: return "Unknown"; } } public static char GetTypecharFromEnum(PluginType T) { switch (T) { case PluginType.Math: return 'M'; case PluginType.Storage: return 'S'; default: return '?'; } } private IPlugin internalPlugin; private string myPath; private PluginType myType = PluginType.Unknown; public string Path { get { return myPath; } } public string Filename { get { return new FileInfo(myPath).Name; } } public PluginType Type { get { if (internalPlugin == null) { return PluginType.Unknown; } return myType; } } public string Name { get { if (internalPlugin == null) { return "Not a recognized plugin"; } return internalPlugin.Name; } } public string Version { get { if (internalPlugin == null) { return ""; } return internalPlugin.Version; } } public string Author { get { if (internalPlugin == null) { return ""; } return internalPlugin.Author; } } public IPlugin PluginInterface { get { return internalPlugin; } } public override string ToString() { return string.Format("{0}: {1}", GetTypecharFromEnum(myType), myPath); } } }
Oof! That's a lot! Fortunately, I typed it all for you. I'll explain it a little bit, and then we'll get on to part 3, where we make this application actually work.
First off, the Get*FromEnum methods are simply there for bookkeeping. They take something that you will probably do a lot (do something different depending on which value the enum is) and put it in one convenient location.
A number of the other properties are just accessors for private data. The only thing of real interest is the internalPlugin field. This is of type IPlugin, and allows you access to all the IPlugin methods. This means that you can do stuff like the Name, Version, and Author properties, which get those values from the plugin that this class encapsulates.