A Few Handy Tips for Using Plugins

Vitalii Symon, 29 October 2010

A week ago I was applying new tax rates to an existing product which was created by Magnetism two years ago.  It took only 3 hours to learn and add new rates, write unit tests and commit a new library for payroll from 1st October 2010. Half a year ago (when Payroll Giving was introduced) it took about same amount of time to apply new rules to the same product - a nice result for an existing product which was not touched for years.

A Few Handy Tips for Using Plugins *

The key is architecture which allows replacing dynamic libraries easily and on-the-fly. There’s no need to rebuild and redeploy whole system to get new taxes working. A bundle of logic library and unit test projects allow making stable libraries without long testing through all the solution layers (UI -> Web Service -> Business Logic -> Data Access) which takes ages to launch.

Changing libraries on-the-fly allows attaching or detaching logic which might vary in time (e.g. tax rules) and depend on country (NZ tax rules or AU tax rules) but operates on the same objects (wage payments).  

So here are the most vital pieces of code to implement this mechanism:

1. Create objects for communications

Remember that this class will be used by big number of components which probably are not designed or even expected to exist now.  If you are not sure about structure of data – just add a generic dictionary (Dictionary<string, object>) which will hold various objects and a method which will cast object to specified type – just for ease of use.

 

        public class MySuperContext
        {
        /// <summary>
        /// Internal presentation of current data set
        /// </summary>

        public Dictionary<string, object> internalDictionary = new Dictionary<string, object>();
        /// <summary>
        /// Collection of all keys present in the dictionary
        /// </summary>

        public Dictionary<strng, object>.KeyCollection AllKeys
        {
            get
            {
            … 
            }
        }

        /// <summary>
        /// Cast requested value to specified type
        /// </summary>
        /// <typeparam name="T">Type to cast the value to</typeparam>
        /// <param name="key">Value key</param>
        /// <returns>Value as T type</returns>
        public T GetAndCast<T>(string key)
        {
       
        }

}     

And write wrappers around it to access often used keys. This will enable intellisense to work with your data.

    public class MyWrapper : DataWrapper
    {
        public static implicit operator MySuperContext (PayeWrapper wr)
        {
            return wr.innerDictionary;
        }

        public decimal MyValue
        {
            get { return innerDictionary.GetAndCast<decimal>("MyValue"); }
            set { innerDictionary["MyValue"] = value; }
        }
    }

Just add properties that will access the dictionary from inherited class. That’s an optional step which will save lots of your time after.

2. Create interface.

The interface should be simple but cover all facilities, replacing it will be painful especially if you allow 3rd-party developers to use it. Do not forget about some identification fields – e.g. GUID or just unique string system name. You can use something like this:

    /// <summary>
    /// All plugins should implement this interface. Plugin must be stateless
    /// </summary>
    public interface IPlugin
    {
        /// <summary>
        /// This name should be unique and is used to identify plugin
        /// </summary>
        string SystemName { get; }

        /// <summary>
        /// This is human-readable name. Might be presented is designers and/or logs
        /// </summary>
        string Name { get; }

 

        /// <summary>
        /// Executes main part of plugin
        /// </summary>
        /// <param name="dictionary">Data to process</param>
        void Process(MySuperContext dictionary);
    }

This interface is extremely simple and has only one method to be launched when the plugin is executed. All data is transferred as dictionary parameter which gives more flexibility.

3. Locate all possible libraries.

This is the most important code which scans a folder and finds all available plugins. To make this happen we have to use reflection. We also have to take into account that plugin is a class, so one .dll file can carry many plugins, so we have to check all classes in the library. Extracting a list of plugins from a binary file look like this:

        /// <summary>
        /// Get all plugins in specified library file
        /// </summary>
        /// <param name="filename">Path to library file</param>
        /// <returns>Plugins list</returns>
        public IEnumerable<IPlugin> GetPlugins(string filename)
        {
            // load assembly
            System.Reflection.Assembly assembly = Assembly.LoadFile(filename);

            // find all appropriate type (that implement IPlugin)
            foreach(Type tp in GetPluginTypes(assembly)) 
                // create and return their instanses
                yield return (IPlugin)tp.GetConstructor(new Type[] { }).Invoke(new object[] { });
        } 

4. Create object instances and invoke interface methods.

That’s the last and pretty simple code for creating instances of plugins. Reflection costs lots of CPU time and memory, so it makes sense to create instances and cache them for further use. Of course it is recommended to make plugins stateless.

        /// <summary>
        /// Enum plugin types in specified assembly
        /// </summary>
        /// <param name="assembly">Assembly to scan</param>
        /// <returns>Plugin types</returns>
        static IEnumerable<Type> GetPluginTypes(Assembly assembly)
        {
            // enum all types
            foreach (Type tp in assembly.GetTypes())
                // check if type implements IPlugin
                if (tp.GetInterfaces().Contains(typeof(IPlugin)))
                    // if yes - return it
                    yield return tp;
        }

The next step is to run those plugins. That’s not hard since you have instances of plugin classes. So just create a foreach loop and execute each plugin. But remember that reflection is pretty slow so it makes sense to cache objects for using again to make system work faster.

We’ve deployed new logic for accounting solution few minutes ago by just replacing dll without even stopping servers, so I’m sure this experience can be useful for others.

* Image from: http://debconf5.debconf.org/living/electricity