MEF and Versioning
Asked Answered
T

2

10

I'm thinking of using MEF to solve a plugin management requirement. In the blurb it says "no hard dependencies" but as far as I can see, there is a hard dependency on the import/export interface.

My concern is this. My extendable app is written by me. Plugins are written by third parties. So lets say we all start off with V1. My app defines a IPlugin interface that the plugin 'parts' need to implement. We deploy the app and users install a bunch of third party plugins. All well and good.

Now I upgrade my app and I want to add a new method to the plugin interface. The way I see it I have 2 choices:

  1. Edit the interface - probably bad, and this would break existing plugins because they would no longer correctly implement the interface.
  2. Create a new 'V2' interface, that inherits from the original

    public interface IPluginV2 : IPlugin {}

Now I have a problem. My users all have a bunch of 3rd party plugins implementing IPlugin, but I now require them to implement IPluginV2. I presume these 3rd party plugins will no longer work, until the developers implement the new interface.

Does MEF have a way to handle this situation? I'm really looking for a way that lets me evolve my app while having old plugins continue to work without having to be rebuilt. Whats the best way of handling that?

Terrarium answered 25/2, 2013 at 1:24 Comment(0)
C
17

For versioning, you will probably want an interface for each version and the adapter pattern to go between them. It is how System.AddIn handles versioning, and it works for MEF, too.

Let's say we have the following types for the V1 of your application:

public interface IPlugin
{
    string Name { get; }
    string Publisher { get; }
    string Version { get; }

    void Init();
}

This is the only contract for our V1 plugin-aware app. It is contained in assembly Contracts.v1.

Then we have a V1 plugin:

[Export(typeof(IPlugin))]
public class SomePlugin : IPlugin
{
    public string Name { get { return "Some Plugin"; } }

    public string Publisher { get { return "Publisher A"; } }

    public string Version { get { return "1.0.0.0"; } }

    public void Init() { }

    public override string ToString()
    {
        return string.Format("{0} v.{1} from {2}", Name, Version, Publisher);
    }
}

Which is exported as IPlugin. It is contained in assembly Plugin.v1 and is published on the "plugins" folder under the application base path of the host.

Finally the V1 host:

class Host : IDisposable
{
    CompositionContainer _container;

    [ImportMany(typeof(IPlugin))]
    public IEnumerable<IPlugin> Plugins { get; private set; }

    public Host()
    {
        var catalog = new DirectoryCatalog("plugins");
        _container = new CompositionContainer(catalog);
        _container.ComposeParts(this);
    }

    public void Dispose() { _container.Dispose(); }
}

which imports all IPlugin parts found in folder "plugins".

Then we decide to publish V2 and because we want to provide versioning we will need versionless contracts:

public interface IPluginV2
{
    string Name { get; }
    string Publisher { get; }
    string Version { get; }
    string Description { get; }

    void Init(IHost host);
}

with a new property and a modified method signature. Plus we add an interface for the host:

public interface IHost
{
    //Here we can add something useful for a plugin.
}

Both of these are contained in assembly Contracts.v2.

To allow versioning we add a plugin adapter from V1 to V2:

class V1toV2PluginAdapter : IPluginV2
{
    IPlugin _plugin;

    public string Name { get { return _plugin.Name; } }

    public string Publisher { get { return _plugin.Publisher; } }

    public string Version { get { return _plugin.Version; } }

    public string Description { get { return "No description"; } }

    public V1toV2PluginAdapter(IPlugin plugin)
    {
        if (plugin == null) throw new ArgumentNullException("plugin");
        _plugin = plugin;
    }

    public void Init(IHost host) { plugin.Init(); }

    public override string ToString() { return _plugin.ToString(); }
}

This simply adapts from IPlugin to IPluginV2. It returns a fixed description and in the Init it does nothing with the host argument but it calls the parameterless Init from the V1 contract.

And finally the V2 host:

class HostV2WithVersioning : IHost, IDisposable
{
    CompositionContainer _container;

    [ImportMany(typeof(IPluginV2))]
    IEnumerable<IPluginV2> _pluginsV2;

    [ImportMany(typeof(IPlugin))]
    IEnumerable<IPlugin> _pluginsV1;

    public IEnumerable<IPluginV2> Plugins
    {
        get
        {
            return _pluginsV1.Select(p1 => new V1toV2PluginAdapter(p1)).Concat(_pluginsV2);
        }
    }

    public HostV2WithVersioning()
    {
        var catalog = new DirectoryCatalog("plugins");
        _container = new CompositionContainer(catalog);
        _container.ComposeParts(this);
    }

    public void Dispose() { _container.Dispose(); }
}

which imports both IPlugin and IPluginV2 parts, adapts each IPlugin into IPluginV2 and exposes a concatenated sequence of all discovered plugins. After the adaptation is completed, all plugins can be treated as V2 plugins.

You can also use the adapter pattern on the interface of the host to allow V2 plugins to work with V1 hosts.

Another approach would be the autofac IoC that can integrate with MEF and can support versioning using adapters.

Canakin answered 25/2, 2013 at 19:54 Comment(1)
Thank you for taking the trouble to post a comprehensive answer. I'm sorry it took me so long to mark this as the accepted answer, but better late than never I guess ;-)Terrarium
G
1

Just a couple of suggestions (which I have not tested) that may help brainstorming a solution for you:

  1. If using MEF, use different AggregateCatalog for each of the versions. That way you could maintain both V1 and V2 plugins available

  2. If not using MEF, a dynamic loaded DLL from the third party should return the current interface version it has implemented and you can choose which calls you could make depending on the version number

Did that help?

Cheers, dimamura

Gerthagerti answered 25/2, 2013 at 10:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.