Search code examples
.netpluginsmef

Manage plugin order and dependency


I am working on a plugin model on .net framework which is based on MEF or unity.

The problem is I haven't found a solution to order plugin execution.

Suppose there exists an execution pipeline which is composed by plugins, there're many kinds of relationships among these plugins: some plugins depend on another plugin that they could only be called after that plugin has been called. Some plugins should be called at the end of the pipeline etc.

The configuration file could be xml or anything else, it's not important. The thing I am confusing is the order algorithm.

A dependent tree could solve, but I don't know if it is enough. Is there any mature solution? Any open source project about this? Or any suggestion?


More explanation.

Suppose I am working on a text editor, this editor supports multiple plugins, after user finish his job and save, a plugin execution pipeline will be called. some plugins work on xaml ,some work on ubb code, and there's a plugin transfer xaml to ubb. So all plugins work on xaml should be called first, and then, call the plugin transfer xaml to ubb and then, call plugins work on ubb. This is an example to plugin dependency and orderring, there may exist more complex relationships among these plugins. So, how to solve this problem in a generic way?


Solution

  • I think what you are looking for is being able to sort by dependencies. I've used something similar whereby I've created a Bootstrapper object to manage application startup. This Bootstrapper supports 0 or more bootstrapper Tasks, which may or may not have dependencies. The way I tackled this was to create a new collection type, a DependencyList<TModel, TKey> which allows you to add an arbitrary number of items, and it will automatically sort on first enumeration, or after any subsequent collection changes.

    In terms of what you want to do, we can take advantage of both this new list type, but also custom MEF export information. The first place we'll start, is our base pipeline plugin contract:

    public interface IPipelinePlugin
    {
        void Process(PipelineContext context);
    }
    
    public abstract class PipelinePluginBase : IPipelinePlugin
    {
        public virtual void Process(PipelineContext context)
        {
    
        }
    }
    

    I prefer to add an abstract class to accompany it, so if I need to, I can introduce some base shared logic without breaking existing plugins.

    Next thing we'll do, define the metadata contract, and then a custom export attribute:

    public interface IPipelinePluginMetadata
    {
        string Name { get; }
        string[] Dependencies { get; }
        string[] Pipelines { get; }
    }
    
    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false)]
    public class PipelineAttribute : ExportAttribute, IPipelinePluginMetadata
    {
        public PipelineAttribute(string name)
            : base(typeof(IPipelinePlugin))
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("A pipeline plugin requires a name.", "name");
    
            Name = name;
        }
    
        public string Name { get; private set; }
        public string[] Dependencies { get; set; }
        public string[] Pipelines { get; set; }
    }
    

    With a custom export attribute, I can define the shape of my exports to ensure they are all exporting the correct information.

    Next, let's look at a custom plugin. Let's assume we want to create a pipeline for applying BBCode adornments to our input text, so first, a simple plugin:

    [Pipeline("ApplyColour", Pipelines = new[] { "bbcode" })]
    public class ApplyColourPipelinePlugin : PipelinePluginBase
    {
        public override void Process(PipelineContext context)
        {
            context.Content = "[color=f00]" + context.Content + "[/color]";
        }
    }
    

    The above example, simply wraps the input text in the [color] tags. The Pipeline attribute details a plugin name (ApplyColour), and what pipelines to make the plugin accessible to, in this case bbcode. Here is a more complex example:

    [Pipeline("MakeBold", Pipelines = new[] { "bbcode" })]
    public class MakeBoldPipelinePlugin : PipelinePluginBase
    {
        public override void Process(PipelineContext context)
        {
            context.Content = "[b]" + context.Content + "[/b]";
        }
    }
    
    [Pipeline("MakeItalic", Dependencies = new[] { "MakeBold" }, Pipelines = new[] { "bbcode" })]
    public class MakeItalicAfterBoldPipelinePlugin : PipelinePluginBase
    {
        public override void Process(PipelineContext context)
        {
            context.Content = "[i]" + context.Content + "[/i]";
        }
    }
    

    In the above example, I'm detailing two additional plugins, one that makes the text bold, and on that is italic. But, I've introduced a dependency requirement, and have told our plugin system, that MakeItalic is dependent on MakeBold. This is how we put it together:

    [Export]
    public class PipelineManager
    {
        [ImportMany]
        public IEnumerable<Lazy<IPipelinePlugin, IPipelinePluginMetadata>> Plugins { get; set; }
    
        public Queue<IPipelinePlugin> BuildPipeline(string name)
        {
            // Get the plugins.
            var plugins = Plugins
                .Where(p => p.Metadata.Pipelines == null || p.Metadata.Pipelines.Contains(name)).ToList();
    
            // Create our dependency list.
            var dependencyList = new DependencyList<Lazy<IPipelinePlugin, IPipelinePluginMetadata>, string>(
                l => l.Metadata.Name,
                l => l.Metadata.Dependencies);
    
            // Add each available plugin to the list.
            plugins.ForEach(dependencyList.Add);
    
            // Create our pipeline.
            var pipeline = new Queue<IPipelinePlugin>();
    
            // Now, when we enumerate over it, it will be sorted.
            dependencyList.ForEach(p => pipeline.Enqueue(p.Value));
    
            return pipeline;
        }
    }
    

    Our PipelineManager type is powered by MEF, so it will [Import] a series of IPipelinePlugin instances along with their associated metadata (which should be shaped to be projectable as IPipelinePluginMetadata). With that in mind, here it is in use:

    class Program
    {
        static void Main(string[] args)
        {
            var container = new CompositionContainer(new AssemblyCatalog(typeof(Program).Assembly));
    
            var manager = container.GetExportedValue<PipelineManager>();
            var pipeline = manager.BuildPipeline("bbcode");
    
            var context = new PipelineContext("Hello World");
    
            foreach (var plugin in pipeline)
                plugin.Process(context);
    
            Console.Write(context.Content);
            Console.ReadKey();
        }
    }
    

    Really the dependency list, and your pipeline design are two separate areas to look at, but I hope this gives you an idea of how you could use it.

    I've thrown this up as a Gist also (https://gist.github.com/1752325).