C# - Plugin architecture sample

Post date: Jan 25, 2010 8:31:57 AM

Plugins are classes that you can add to an application without modifying the application itself. The idea is that each plugin implement an interface with methods that the plugins are allowed to use. These classes need to be compiled into DLLs or exe:s. For the application to know which classes within an assembly it can load the plugin need to implement the interface, so the interface have double purpose.

The idea of this application is to have a scheduler that will trigger tasks on a scheduled basis. These tasks could be things like sending files, running reports etc. Each task is implemented as a plugin. The scheduler application/service does never need to be stopped or modified when adding new tasks. The configuration of the tasks and schedule is done in the database but the database tables are not included in this example.

Overview:

ISchedulePlugin is the interface that the plugins have to implement to be able to be loaded by this sample application. The plugins can Run, and report events on errors, completed and progress. The application can listen to these events and log or send Emails to someone who cares.

PluginManager is responsible for loading the Plugins from the assemblies in the plugin folder.

BasePlugin have most of the common methods in the interface implemented so that the coder of new plugins only would need to implement the Run(..) method.

Scheduler is the application in this case. It will read configuration from some database to know when which plugin should run. For the scheduler to know when to run what, it will need to schedule a SchedulingPlugin for itself.

using System;namespace MrM.Scheduler.PluginInterfaces

{ public interface ISchedulePlugin : IComparable, IDisposable

{ /// <summary> /// Executes the main functionallity of the plugin /// </summary> /// <param name="connectionString">A connectionstring to the Schedule-database where the plugin can find it's own connectionstrings</param> /// <param name="date">A date that can be used as a parameter to the task</param> /// <param name="startArgument">a string that can include the arguments needed to perform the intended task. /// This value is configured in the Schedule database</param> void Run(string connectionString, DateTime date, string startArgument); /// <summary> /// A name of the plugin so it can implement Equals() and CompareTo() /// </summary> string Identifier { get; } /// <summary> /// Disposing the object. /// </summary> void Dispose(); /// <summary> /// To be invoked when there is an error occur /// </summary> event ErrorHandler OnError; /// <summary> /// To be invoked when the plugin have finished with no problem. /// </summary> event CompletedHandler OnCompleted; /// <summary> /// To be invoked when the plugin need to report some kind of progress. Used for logging /// </summary> event ProgressReportHandler OnProgress; } }

using System;using System.Reflection;using MrM.Scheduler.PluginInterfaces;namespace MrM.Scheduler

{ /// <summary> /// Responsible for loading Plugins from assemblies /// </summary> public class PluginManager

{ /// <summary> /// Singleton Instance access to the object /// </summary> private static PluginManager _instance; /// <summary> /// Singleton Instance access to the object /// </summary> public static PluginManager Instance

{ get { if (_instance == null) _instance = new PluginManager(); return _instance; } } /// <summary> /// private constructor for Singleton pattern /// </summary> PluginManager() { } /// <summary> /// Will load and create an instance of a ISchedulePlugin from assembly file name /// </summary> /// <remarks> /// If the assembly contains more than one plugin only the first one found will be returned. /// </remarks> /// <param name="assemblyName">The name of the Assembly to load the plugin from</param> /// <returns>An instance of the plugin or null if no plugins were found</returns> public ISchedulePlugin LoadPlugin(string assemblyName) { try { string assemblyFileName = String.Format("{0}\\Plugins\\{1}", System.Environment.CurrentDirectory, assemblyName); Assembly asm = Assembly.LoadFrom(assemblyFileName); Type[] types = asm.GetTypes(); foreach (Type thisType in types) { Type[] interfaces = thisType.GetInterfaces(); foreach (Type thisInterface in interfaces) { if (thisInterface.Name == "ISchedulePlugin") { if (thisInterface.Namespace == "MrM.Scheduler.PluginInterfaces") { //-- Load the object object obj = asm.CreateInstance(String.Format("{0}.{1}", thisType.Namespace, thisType.Name)); return obj as ISchedulePlugin; } } } } } catch { // This should be logged } return null; } /// <summary> /// Checks the Plugin folder for the specified assembly. /// If the assembly is found it is loaded and checked weather it contains a plugin or not. /// The plugin need to implement the ISchedulePlugin interface to be a valid plugin. /// </summary> /// <param name="assemblyName">The Dll that should be tested</param> /// <returns>true if the DLL can be found and it contains a Plugin, else false</returns> public bool CanFindPlugin(string assemblyName) { if (LoadPlugin(assemblyName) != null) return true; else

return false; } }}

using System;namespace MrM.Scheduler

{ public static class PluginEventHandler

{ public static void Plugin_OnCompleted(string message) { Console.WriteLine(String.Format("Completed {0}", message)); } public static void Plugin_OnError(string message, MrM.Scheduler.PluginInterfaces.Severity errorSeverity) { Console.WriteLine(String.Format("{0} {1}", message, errorSeverity)); } public static void Plugin_OnProgress(string message, int percentCompleted) { Console.WriteLine(message); } }}

using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace MrM.Scheduler.PluginInterfaces

{ public abstract class BasePlugin : ISchedulePlugin

{ /// <summary> /// An identifier to know what plugin this is. Used to implement <see cref="CompareTo"/>, <see cref="Equals"/> etc. /// </summary> protected string _identifier; /// <summary> /// The main function that the scheduler will invoke at the scheduled time. /// </summary> /// <param name="date">Scheduler will normally send DateTime.Now</param> /// <param name="startArgument">the string of parameters that the plugin might need to perform it's task</param> public abstract void Run(string connectionString, DateTime date, string startArgument); public string Identifier

{ get { return _identifier; } } /// <summary> /// Sends the OnError Event /// </summary> /// <param name="message">The message to send</param> /// <param name="errorSeverity">An indication on what kind of error this is</param> protected void ReportError(string message, Severity errorSeverity) { if (OnError != null) OnError(message, errorSeverity); } /// <summary> /// Sends the OnProgress Event. /// </summary> /// <param name="message">The message to send</param> /// <param name="percentCompleted">Indicates how much progress have been made</param> protected void ReportProgress(string message, int percentCompleted) { if (OnProgress != null) OnProgress(message, percentCompleted); } /// <summary> /// Sends the OnProgress Event /// </summary> /// <param name="message">The messeage to send</param> protected void ReportProgress(string message) { ReportProgress(message, 0); } /// <summary> /// Sends the OnCompleted Event /// </summary> /// <param name="message">The message to send</param> protected void ReportCompleted(string message) { if (OnCompleted != null) OnCompleted(message); } /// <summary> /// Compare this instance to another object. /// </summary> /// <param name="obj">The other object to compare this instance with.</param> /// <returns></returns> public int CompareTo(object obj) { return _identifier.CompareTo(obj); } /// <summary> /// Checks if this object is value equal as other object /// </summary> /// <param name="obj">Other object to compare</param> /// <returns></returns> public override bool Equals(object obj) { return _identifier.Equals(obj); } /// <summary> /// Get the hashcode of this object /// </summary> /// <returns>the hashcode of the object</returns> public override int GetHashCode() { return _identifier.GetHashCode(); } /// <summary> /// Disposes of the instance /// </summary> public void Dispose() { OnError = null; OnCompleted = null; OnProgress = null; _identifier = null; } /// <summary> /// Checks if two instances of plugins are the same, reference equal and value equal. /// </summary> /// <param name="a">first instance</param> /// <param name="b">second instance</param> /// <returns></returns> public static bool operator ==(BasePlugin a, BasePlugin b) { // If both are null, or both are same instance, return true. if (System.Object.ReferenceEquals(a, b)) { return true; } // If one is null, but not both, return false. if (((object)a == null) || ((object)b == null)) { return false; } // Return true if the fields match: return a._identifier == b._identifier; } /// <summary> /// Not equal <see cref="=="/> /// </summary> /// <param name="a">first instance</param> /// <param name="b">second instance</param> /// <returns></returns> public static bool operator !=(BasePlugin a, BasePlugin b) { return !(a == b); } // Events are described in ISchedulePlugin interface public event ErrorHandler OnError; public event CompletedHandler OnCompleted; public event ProgressReportHandler OnProgress; }}

namespace MrM.Scheduler.PluginInterfaces

{ public delegate void ErrorHandler(string message, Severity errorSeverity); public delegate void CompletedHandler(string message); public delegate void ProgressReportHandler(string message, int percentCompleted);}

using System.Collections;using System;namespace MrM.Scheduler.PluginInterfaces

{ public class PluginCollection : CollectionBase

{ public int Add(ISchedulePlugin aIPlugin) { return InnerList.Add(aIPlugin); } public void Insert(int index, ISchedulePlugin aIPlugin) { InnerList.Insert(index, aIPlugin); } public void Remove(ISchedulePlugin aIPlugin) { InnerList.Remove(aIPlugin); } public ISchedulePlugin Find(string pluginName) { foreach (ISchedulePlugin plugin in this) { if (plugin.Identifier == pluginName) { return plugin; } } return null; } public bool Contains(string MenuText) { return (Find(MenuText) != null); } public ISchedulePlugin this[int index]

{ get { return (ISchedulePlugin)base.InnerList[index]; } } }}

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Data.SqlClient;namespace MrM.Scheduler

{ public class Scheduler

{ /// <summary> /// The timers used to periodically check if some plugin is due to run. /// </summary> private System.Timers.Timer _timer; /// <summary> /// The main schedule containing all the scheduled plugins. /// </summary> private List<PluginSchedule> _schedules; /// <summary> /// Constructor that will load the schedule for plugins that are scheduled to run Today /// </summary> public Scheduler() { _schedules = new List<PluginSchedule>(); LoadScheduleForToday(); } /// <summary> /// Loads the schedule for plugins that should run today. /// </summary> private void LoadSchedule() { string connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["ScheduleDB"].ConnectionString; using (SqlConnection conn = new SqlConnection(connectionString)) { using (SqlCommand cmd = new SqlCommand(@"select

p.PluginId, p.AssemblyName, p.PluginName, s.ExecutionTime, par.ParameterString

from

Plugins p

inner join tx_DailySchedule s

on p.PluginID = s.PluginId

inner join PluginParameters par

on p.PluginId = par.PluginId", conn)) { conn.Open(); SqlDataReader reader = cmd.ExecuteReader(); // The actuall loading of the plugins is not done until its time to execute them. while (reader.Read()) { _schedules.Add(new PluginSchedule( reader["AssemblyName"].ToString(), connectionString, (DateTime)reader["ExecutionTime"], reader["ParameterString"].ToString() )); } } } // Add event handlers foreach (var item in _schedules) { //item.Plugin.OnProgress += Plugin_OnProgress; //item.Plugin.OnError += Plugin_OnError; //item.Plugin.OnCompleted += Plugin_OnCompleted; } } private void LoadScheduleForToday() { // fyll _schedules från databasen med dagens händelser //PluginManager.Instance.RefreshPlugins(); //foreach (var item in PluginManager.Instance.Plugins) //{ //} LoadSchedule(); } public void Start() { _timer = new System.Timers.Timer(); _timer.Interval = 1000; _timer.AutoReset = true; _timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); _timer.Start(); } void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { List<PluginSchedule> toBeRemoved = new List<PluginSchedule>(); foreach (var item in _schedules) { DateTime dateTimeNow = DateTime.Now; if (item.ExecutionStartTime.IsTimeToRun(dateTimeNow)) { item.RunScheduledPlugin(); toBeRemoved.Add(item); } } foreach (var item in toBeRemoved) { _schedules.Remove(item); } } public void Stop() { foreach (var item in _schedules) { item.Dispose(); } //PluginManager.Instance.Plugins.Clear(); } }}

The scheduler

using System;using System.Collections.Generic;using System.Linq;using System.Text;using MrM.Scheduler.PluginInterfaces;using System.Threading;namespace MrM.Scheduler

{ public sealed class PluginSchedule : IDisposable

{ /// <summary> /// The plugin that should be invoked /// </summary> public ISchedulePlugin Plugin { get; set; } /// <summary> /// The time when the plugin should be started /// </summary> public ExecutionTime ExecutionStartTime { get; set; } /// <summary> /// The (main) thread that the plugin will run in. /// </summary> private Thread _scheduleThread { get; set; } /// <summary> /// String that can contain parameters needed for the plugin to run. /// </summary> string _parameterString; /// <summary> /// The name of the DLL that the plugin can be loaded from /// </summary> private string _assemblyName; string _connectionString; /// <summary> /// Constructor. Sets up all the needed information for the plugin to run /// </summary> /// <param name="assemblyName">the assembly file name where the plugin is located</param> /// <param name="connectionString">the connectionstring that the plugin can use to load additional configurations from</param> /// <param name="timeToRun">the time when the plugin should be started</param> /// <param name="parameterString">a string that can contain parameters needed for the plugin to run</param> public PluginSchedule(string assemblyName, string connectionString, DateTime timeToRun, string parameterString) { _assemblyName = assemblyName; ExecutionStartTime = new ExecutionTime(timeToRun); _parameterString = parameterString.Clone() as string; _connectionString = connectionString.Clone() as string; // Check if the plugin can be found. But do not load and save the reference for it. if (! PluginManager.Instance.CanFindPlugin(_assemblyName)) { // Detta skall loggas istället throw new Exception(string.Format("could not load {0}", _assemblyName)); } // When the scheduler starts this plugin, the plugin will load from its assembly and its Run-method is invoked. // This is done in a seperate thread to not disturb the scheduler. _scheduleThread = new Thread(delegate()

{ Plugin = PluginManager.Instance.LoadPlugin(_assemblyName); Plugin.OnProgress += PluginEventHandler.Plugin_OnProgress; Plugin.OnError += PluginEventHandler.Plugin_OnError; Plugin.OnCompleted += PluginEventHandler.Plugin_OnCompleted; Plugin.Run(_connectionString, DateTime.Now, _parameterString); Plugin.OnProgress -= PluginEventHandler.Plugin_OnProgress; Plugin.OnError -= PluginEventHandler.Plugin_OnError; Plugin.OnCompleted -= PluginEventHandler.Plugin_OnCompleted; Plugin.Dispose(); }); } /// <summary> /// Starts the thread that will load and run the plugin. /// </summary> public void RunScheduledPlugin() { _scheduleThread.Start(); } #region IDisposable Members /// <summary> /// Disposes of the object and all its children /// </summary> public void Dispose() { _scheduleThread.Abort(); Plugin.Dispose(); _scheduleThread = null; } #endregion }}

Now, an implementation of the ISchedulePlugin-interface. This is the actuall plugin.

using System;using System.Linq;using MrM.Scheduler.PluginInterfaces;using System.Data.SqlClient;using System.Data;using System.Xml.Linq;namespace MrM.Scheduler.XML_Posten_Plugin

{ public class PostenPlugin : BasePlugin

{ public PostenPlugin() { _identifier = "XML_Posten_Plugin"; } public override void Run(string connectionString, DateTime date, string startArgument) { try { ReportProgress("Starting making xml file for posten", 0); CreateXMLFile(); ReportCompleted("Done with xml file"); } catch (Exception e) { ReportError("Error while creating file: " + e.ToString(), Severity.Error); } } void CreateXMLFile() { using (SqlConnection conn = new SqlConnection(@"server=localhost; Integrated Security=true; Trusted_Connection=yes; database=Database; connection timeout=30")) { conn.Open(); using (SqlCommand cmd = new SqlCommand(@"

select

* from orders

", conn)) { DateTime date = DateTime.Now;// "20080910"; SqlDataAdapter ad = new SqlDataAdapter(cmd); ad.SelectCommand.Parameters.AddWithValue("date", date.ToString("yyyyMMdd")); DataSet ds = new DataSet(); ad.Fill(ds); string longDate = UppercaseWords(date.ToString("MMM, dd MMM yyyy") + " 10:00:00 +0100"); var m = new XDocument( new XDeclaration("1.0", "iso-8859-1", "yes"), new XProcessingInstruction("POSTNET", "SND=\"5562211697\" SNDKVAL=\"30\" REC=\"0007556451TEST\" RECKVAL=\"30\" MSGTYPE=\"INSTRXML\""), new XElement("instruction", new XElement("header", new XElement("version", "1.0"), new XElement("sender", new XAttribute("id", "5562211697"), new XAttribute("codelist", "30") ), new XElement("receiver", new XAttribute("id", "00075564514106"), new XAttribute("codelist", "30") ), new XElement("interchangetime", longDate), new XElement("interchangeid", "1000000096"), new XElement("applicationid", "TheSystem") ), from DataRow f in ds.Tables[0].Rows

select new XElement("consignment", new XAttribute("functioncode", "9"), new XElement("date", date.ToString("yyyMMdd")), new XElement("service", new XAttribute("code", f["ServiceCode"]) ), new XElement("consignor", new XAttribute("id", "1200133005"), new XAttribute("name", "Mygel Inc."), new XElement("address", new XElement("street", "Timmervägen 8"), new XElement("postcode", "85753"), new XElement("city", "SUNDSVALL"), new XElement("countrycode", "SE") ), new XElement("contact", new XElement("communication", "0855611615", new XAttribute("type", "TE") ) ) ), new XElement("consignee", new XAttribute("name", f["PersonName"]), new XElement("address", new XElement("street", f["Address"]), new XElement("postcode", f["PostalCode"]), new XElement("city", f["City"]), new XElement("countrycode", f["CountryCode"]) ), new XElement("contact", new XElement("communication", f["PhoneNumber"], new XAttribute("type", "TE") ) ) ), new XElement("parcel", new XAttribute("id", f["ParcelID"]), new XElement("weight", "1.00") ) ) )); //Console.WriteLine(m.ToString()); m.Save("c:\\temp\\test.xml"); } } } /// <summary> /// Uppercases the first letter in each word of the string /// </summary> /// <param name="value"></param> /// <returns></returns> static string UppercaseWords(string value) { // Strings cannot be modified, creating an array of chars that CAN be modified. char[] array = value.ToCharArray(); // Handle the first letter in the string. if (array.Length >= 1) { if (char.IsLower(array[0])) { array[0] = char.ToUpper(array[0]); } } // Scan through the letters, checking for spaces. // ... Uppercase the lowercase letters following spaces. for (int i = 1; i < array.Length; i++) { if (array[i - 1] == ' ') { if (char.IsLower(array[i])) { array[i] = char.ToUpper(array[i]); } } } return new string(array); } }}