Introduction
In this article, I will introduce plug-in based development and explain a simple library named SPS (Simple Plug-In System) that is used to develop plug-in based applications and services in .NET.
Article Outline
A few month ago, when I was developing a Windows service that performs some background operations, I needed to extend the service to add some customer specific codes. It was not proper to mix customer specific codes with my core application since these codes/classes can be removed later or I can add some codes for another customer too. So, my core application has to be independent from customer codes but must provide some common substructure for them. I decided to build my application as plug-in based. So, my customer specific codes are implemented as independent plugins.
I developed a Simple Plug-In System for this purpose, then I used it in my other projects too. In this article, I will share my simple and flexible work and will explain how I implemented it.
In computing, a plug-in is a set of software components that adds specific abilities to a larger software application [1]. You can allow to extend your application without changing or even re-compiling it by designing it as plug-in based. You can see a basic structure of an application - plugin relation in the figure below:
Figure 1: Basic architecture of a plug-in based application (from Wikipedia) [2].
Since the host application is independent from all plug-ins and cannot know how they are implemented, it defines a plug-in interface that must be implemented by all plug-ins. Typically, a plug-in manager in the host application used to load and manage all plug-ins. In the simple case, all plug-ins are placed in a specific plugin directory, plug-in manager loads all assemblies (generally DLL projects) in the plug-in directory and searches for classes those implementing the plug-in interface.
Plug-in manager may load plug-ins for once on startup of the host application in most simple case. Another approach may be to load/unload plug-ins dynamically while the application is running. Even more, an application can update plug-ins while running. Thus, a plug-in can be updated without restarting the main application. Host application can use plug-ins over plug-in interface after loading plug-ins.
In many applications, a plug-in may also need to use some functionalities of the host application. So, the application may provide an interface to plug-ins that can be named as service interface or application interface. Thus, a plug-in can call a method of the host application when needed.
Another concept that is considered while developing plug-in based applications is Application Domains. Application domains provide an isolation boundary for security, reliability, versioning and for unloading assemblies [3]. A .NET process has single application domain by default. You can create other application domains in runtime.
An assembly cannot be unloaded until the application domain (that hosts it) unloaded. So, if you loaded a plug-in assembly in your default application domain, it cannot be unloaded (thus, assembly file cannot be changed in file system) until your application is closed. If you create a separated application domain and load plug-ins into this domain, then you can safely unload/reload/change the assembly without restarting your main application. But, unfortunately, you cannot directly use objects in other application domains and must use .NET remoting, marshalling and serializing techniques. If you don't really need to unload/update your plugins on runtime, you don't have to use application domains (In most scenarios, it is not needed). SPS also does not use application domains.
SPS is a simple and open source library that implements exactly the model in the figure 1 to provide an infrastructure for building plug-in based applications in .NET.
We will develop a very simple calculator application to demonstrate the usage of SPS library.
First, let me show how the calculator works:
Figure 2: A plug-in based calculator application.
Calculator application has a simple GUI. You simply enter two numbers, select operation and press the Calculate button. Also, you see all loaded plug-ins on the right side of the form.
Calculator application does not provide any operation itself. Operations can be added as plug-ins without changing the calculator application. In figure 2, three plug-ins are used to perform three operations (Addition
, Division
and Multiply
). Operation combobox is filled dynamically according to currently loaded plug-ins. Plug-ins are placed in PlugIns folder in root application folder.
There are two projects in the calculator solution:
Figure 3: Calculator application solution.
CalculatorHostApp
is the main application that provides the GUI. CalculatorPlugInLib
contains plug-in interface and application interface. Both projects add SPS.dll (Simple Plug-In System) to references to be able to use plug-in system.
We need a sign/char to show operation to the user (in operation combobox) and a method to calculate result for this operation:
using Hik.Sps;
namespace CalculatorPlugInLib
{
public interface ICalculatorOperationPlugIn : IPlugIn
{
string OperationSign { get; }
double DoOperation(double number1, double number2);
}
}
This interface is used by the calculator application and implemented by all plug-ins.
Maybe an application interface is not needed in a calculator application but I will define to demonstrate the usage of application interfaces. In SPS, you must provide an application interface even it has no member (an empty interface). Our application provides only one method to plug-ins:
using Hik.Sps;
namespace CalculatorPlugInLib
{
public interface ICalculatorApplication : IPlugInBasedApplication
{
void ShowMessage(string message);
}
}
We must implement the ICalculatorApplication
interface in the calculator application:
using System.Windows.Forms;
using CalculatorPlugInLib;
using Hik.Sps;
namespace CalculatorHostApp
{
[PlugInApplication("Calculator")]
internal class CalculatorPlugInManager :
PlugInBasedApplication<ICalculatorOperationPlugIn>, ICalculatorApplication
{
public void ShowMessage(string message)
{
MessageBox.Show(message);
}
}
}
CalculatorPlugInManager
is the main class that is used to load and manage plug-ins. It implements ICalculatorApplication
to be used by plug-ins. It also defines PlugInApplication
attribute that is used by SPS to get the name of the application (not a must, but recommended).
It is inherited from PlugInBasedApplication<ICalculatorOperationPlugIn>
. This is the key point of the class. PlugInBasedApplication
provides methods/properties to load and access to plug-ins. It is a generic class that takes plug-in interface type as generic parameter.
Let's see how CalculatorPlugInManager
is used in calculator application. First, we define an object from CalculatorPlugInManager
:
private CalculatorPlugInManager _plugInManager;
Then we load plugins and fill combobox and listbox in the form's constructor:
_plugInManager = new CalculatorPlugInManager();
_plugInManager.PlugInFolder = Path.Combine(GetCurrentDirectory(), "PlugIns");
_plugInManager.LoadPlugIns();
foreach (var plugIn in _plugInManager.PlugIns)
{
cmbOperation.Items.Add(plugIn.PlugInProxy.OperationSign);
lstPlugIns.Items.Add(plugIn.Name);
}
if (cmbOperation.Items.Count > 0)
{
cmbOperation.SelectedIndex = 0;
}
else
{
btnCalculate.Enabled = false;
}
LoadPlugIns()
method loads all plug-ins from given PlugInFolder directory. We use PlugInProxy
property to use the proxy interface. Finally, when user clicks the Calculate
button, we find the right plug-in and calculate the result:
private void btnCalculate_Click(object sender, EventArgs e)
{
var number1 = Convert.ToDouble(txtNumber1.Text);
var number2 = Convert.ToDouble(txtNumber2.Text);
var operation = cmbOperation.SelectedItem.ToString();
var result = Calculate(operation, number1, number2);
lblResult.Text = result.ToString("0.0");
}
public double Calculate(string operation, double number1, double number2)
{
foreach (var plugIn in _plugInManager.PlugIns)
{
if (plugIn.PlugInProxy.OperationSign == operation)
{
return plugIn.PlugInProxy.DoOperation(number1, number2);
}
}
throw new ApplicationException
("Can not found any plug-in for operation " + operation);
}
PlugIns collection of plug-in manager contains all plug-ins. In the Calculate
method, we search all plug-ins to find proper operation and call DoOperation
method of the plug-in to calculate the result. That's all!
Our plug-in infrastructure is ready to build plug-ins for the calculator application. We can add as many operations as we want without changing the calculator application. This is extensibility! Now, we can create a new solution to build some plug-ins:
Figure 4: Plug-ins for calculator.
Our plug-ins depends on two library: SPS.dll and CalculatorPlugInLib.dll. You can define more than one plug-in in an assembly. I created three plug-ins in two assemblies.
Let's see AddOperation
plug-in:
using Hik.Sps;
using CalculatorPlugInLib;
namespace Add
{
[PlugIn("Addition")]
public class AddOperation :
PlugIn<ICalculatorApplication>, ICalculatorOperationPlugIn
{
public string OperationSign
{
get { return "+"; }
}
public double DoOperation(double number1, double number2)
{
return number1 + number2;
}
}
}
A plug-in has a PlugIn
attribute (This attribute defines the name of the plug-in in its constructor. It is not a must, but recommended). Addition Plug-in implements ICalculatorOperationPlugIn
as expected. It is also derived from PlugIn<ICalculatorApplication>
class. PlugIn
class provides base functionallity for plug-ins. It gets the application interface as generic parameter.
Let's see DivisionOperation
plug-in now:
using Hik.Sps;
using CalculatorPlugInLib;
namespace MultiplyAndDivision
{
[PlugIn("Division")]
public class DivisionOperation :
PlugIn<ICalculatorApplication>, ICalculatorOperationPlugIn
{
public string OperationSign
{
get { return "/"; }
}
public double DoOperation(double number1, double number2)
{
if (number2 == 0.0)
{
Application.ApplicationProxy.ShowMessage
("Second number can not be zero in division!");
return 0.0;
}
return number1 / number2;
}
}
}
You see how to call ShowMessage
method of application interface of the calculator. Application.ApplicationProxy
property is used to use application interface from plug-ins. There is also another plug-in (MultiplyOperation
) in MultiplyAndDivision
project.
After creating and compiling plug-ins, we must place them into PlugIns folder of the calculator application. You can see the source code under Samples\Calculator folder in the download file.
I will explain some key parts of implementation of SPS library. The main class that loads plug-ins is PlugInBasedApplication<TPlugIn>
. Its LoadPlugIns()
method is shown below:
public void LoadPlugIns()
{
if (string.IsNullOrEmpty(PlugInFolder) || !Directory.Exists(PlugInFolder))
{
throw new ApplicationException("PlugInFoler must be a valid folder path");
}
var assemblyFiles = SpsHelper.FindAssemblyFiles(PlugInFolder);
var plugInType = typeof(TPlugIn);
foreach (var assemblyFile in assemblyFiles)
{
var allTypes = Assembly.LoadFrom(assemblyFile).GetTypes();
foreach (var type in allTypes)
{
if (plugInType.IsAssignableFrom(type) && type.IsClass && !type.IsAbstract)
{
PlugIns.Add(new ApplicationPlugIn<TPlugIn>(this, type));
}
}
}
PlugInsLoaded = true;
}
We simply get all DLL and EXE files in plug-in folder (using SpcHelper.FindAssemblyFiles(...)
method). For each assembly, we get all types in the assembly and add types to PlugIns collection that implements the plug-in interface. We also check if the type is a concrete class (not abstract
). As we see in calculator sample, we inherit PlugInBasedApplication
class when creating a plug-in manager.
We create a new ApplicationPlugIn<TPlugIn>
object for each plug-in type in LoadPlugIns()
method. Let's see this class:
using System;
namespace Hik.Sps
{
internal class ApplicationPlugIn<TPlugIn> : IApplicationPlugIn<TPlugIn>
{
public string Name { get; private set; }
public TPlugIn PlugInProxy { get; private set; }
public ApplicationPlugIn
(IPlugInBasedApplication plugInApplication, Type plugInType)
{
PlugInProxy = (TPlugIn)Activator.CreateInstance(plugInType);
var plugInObjectType = PlugInProxy.GetType();
var applicationProperty = plugInObjectType.GetProperty("Application");
var applicationPropertyValue =
applicationProperty.GetValue(PlugInProxy, null);
var applicationPropertyType = applicationPropertyValue.GetType();
applicationPropertyType.GetProperty("Name").SetValue
(applicationPropertyValue, plugInApplication.Name, null);
applicationPropertyType.GetProperty
("ApplicationProxy").SetValue
(applicationPropertyValue, plugInApplication, null);
Name = ((IPlugIn) PlugInProxy).Name;
}
}
}
Constructor of this class is important. It creates the plug-in object and sets Application
property using reflection techniques. The last class that will be explained is PlugIn
class that is inherited by all plug-ins:
namespace Hik.Sps
{
public abstract class PlugIn<TApp> : IPlugIn
{
public IPlugInApplication<TApp> Application { get; internal set; }
public string Name { get; private set; }
protected PlugIn()
{
Application = new PlugInApplication<TApp>();
var thisPlugInType = GetType();
var plugInAttribute = SpsHelper.GetAttribute<PlugInAttribute>(thisPlugInType);
Name = plugInAttribute == null ? thisPlugInType.Name : plugInAttribute.Name;
}
}
}
PlugIn<TApp>
class gets the name of the plug-in from its PlugIn
attribute. If there is no PlugIn
attribute, it gets the name of the derived class as Name of the plug-in. So, PlugIn
attribute is not a must.
Lastly, I will mention about some usable classes in SPS library. If you want to develop some services/modules that can be started/stopped and want to develop these modules as plug-in to a main application, you can use ready-to-use classes in Runnable
namespace of SPS. You can derive your plug-ins from RunnablePlugIn
class and derive your plug-in manager from RunnablePlugInBasedApplication
class. Although you can easily build your own classes like that, you can take a look at these ready classes.
Source on github
Source code is published on github: https://github.com/hikalkan/sps. You can fork and send pull requests.
[1] http://en.wikipedia.org/wiki/Plug-in_(computing)
[2] http://en.wikipedia.org/wiki/File:Plug-InExample.png
[3] http://msdn.microsoft.com/en-us/library/2bh4z9hs.aspx (Application Domains, MSDN)
- 21th May, 2014: Added source codes to github
- 17th April, 2011: Initial version
- 18th April, 2011: Updated download file