.NET AddIn Framework With Backwards Compatibility

Posted on: February 24, 2016 6:30:27 PM

At work with my teammate, Jeremy, the application we're working on currently supports plugins built by 3rd parties. The problem we started running into once we thought about making it public was that anytime we try to update the libraries, it would break any plugin that was developed that wasn't compiled using those libraries. To solve this problem, we started looking into the .NET AddIn Framework that can support backwards compatibility. Figuring out this framework took quite a bit of work, there isn't a ton of documentation nor are there many examples of what we needed. It's relatively easy to have the Host application call into a Plugin, but quite a bit more complex if you want to support the reverse. The whole process is still quite complex and takes some time to wrap your head around, I'll post some of the important code here and then a link to my test project that has the fully working source and a test project to demonstrate.

To setup the basic Host-to-AddIn structure, we followed this MSDN Example. Once you get that working, in order to get the callback to the Host setup, you need to add a new contract and modify your existing contract to support passing in a reference to your Host's handler.

ICallbackContract
using System.AddIn.Contract;

namespace ContractV2.V2
{
    public interface ICallbackContractV2 : IContract
    {
        void DoWork();
    }
}
IContractV2
using System.AddIn.Contract;
using System.AddIn.Pipeline;

namespace ContractV2.V2
{
    [AddInContract]
    public interface IContractV2 : IContract
    {
        string GetName();

        void Initialize(ICallbackContractV2 callback);

        void WriteToConsole(string output);

        object GetSource();
    }
}

You can follow the MSDN tutorial once again to propagate this contract all the way through the project, it's virtually the same with the exception of your callback interfaces and adapters don't require the same Attributes that your first contract will because it isn't technically a full AddIn contract. The next step is to create the normal AddInAdapater.

AddinAdapterV2
using AddinViewV2.V2;
using ContractV2.V2;
using System.AddIn.Pipeline;

namespace AddinAdapterV2.V2
{
    [AddInAdapter]
    public class AddinAdapter : ContractBase, IContractV2
    {
        private IV2 _view;

        public AddinAdapter(IV2 view)
        {
            _view = view;
        }

        public string GetName()
        {
            return _view.GetName();
        }

        public void Initialize(ICallbackContractV2 callback)
        {
            _view.Initialize(CallbackConverter.FromContract(callback));
        }

        public void WriteToConsole(string output)
        {
            _view.WriteToConsole(output);
        }

        public object GetSource()
        {
            return _view.GetSource();
        }
    }
}

The important thing to note in this class is the converter. This is needed to translate the contract to the callback.

CallbackConverter
using AddinViewV2.V2;
using ContractV2.V2;

namespace AddinAdapterV2.V2
{
    public class CallbackConverter
    {
        internal static ICallbackV2 FromContract(ICallbackContractV2 callback)
        {
            return new CallbackImpl(callback);
        }
    }
}

In order for this converter to work is to have a concrete implementation of the class you're converting to.

CallbackImpl
using AddinViewV2.V2;
using ContractV2.V2;

namespace AddinAdapterV2.V2
{
    public class CallbackImpl : ICallbackV2
    {
        private ICallbackContractV2 _contract;

        public CallbackImpl(ICallbackContractV2 contract)
        {
            _contract = contract;
        }

        public void DoWork()
        {
            _contract.DoWork();
        }
    }
}

You probably have noticed that my interfaces are listed as V2. This is because I also wanted to show what an upgrade scenario looked like and how to handle conversions to support backwards compatibility. To do this, you'll need to create a new Adapter project to handle the conversion.

AddinAdapterV1ToV2
using AddinView.V1;
using ContractV2.V2;
using System.AddIn.Pipeline;
using System.Diagnostics;

namespace AddinAdapterV1ToV2.V2
{
    [AddInAdapter]
    public class AddinAdapterV1ToV2 : ContractBase, IContractV2
    {
        private IV1 _view;

        public AddinAdapterV1ToV2(IV1 view)
        {
            _view = view;
        }

        public string GetName()
        {
            return _view.GetName();
        }

        public void Initialize(ICallbackContractV2 callback)
        {
            _view.Initialize(CallbackConverter.FromContractV2(callback));
        }

        public void WriteToConsole(string output)
        {
            Debug.WriteLine("Outout is ignored: ", output);
            _view.WriteToConsole();
        }

        public object GetSource()
        {
            return null;
        }
    }
}

Here you'll notice that this class implements the IContractV2 interface but the constructor requires a IV1 interface to be passed. This is what will convert a V1 Plugin to the current Host, allowing for reverse compatibility. You can also do the inverse where you convert the Host to a new Plugin; however, this is only required if you'll be releasing these libraries separate from your Host application. In my code example, I did this just to show what is necessary.

The next issue we ran into was that because it used to be tightly integrated, there were some objects that were passed around by reference. We couldn't get away from this model without a lot of restructuring and re-architecting the base code. We found that the AddIn framework Activator allowed you to specify the AppDomain. So we could use that to load the AddIn into the same AppDomain as the existing project which allows us to once again pass by reference without the need of serialization like you'd normally have to.

Program snippet
                IV2 v2 = token.Activate(AppDomain.CurrentDomain);
                v2.Initialize(new CallbackHandler());

                // Run the add-in.
                v2.WriteToConsole("Hello World From Host!");
                Console.WriteLine(v2.GetName());

                var test = (Stopwatch)v2.GetSource();

                Task.Delay(500).Wait();

                test.Stop();
                Console.WriteLine(test.ElapsedTicks);

You'll notice in the contract that the GetSource method returns an object. I've set it up to return a Stopwatch, which isn't serializable and wouldn't normally be allowed to pass through this framework. By loading the AddIn into the same AppDomain, this works.

The full code base can be found here. This is only a brief description over the trouble areas that I came across. If you have any questions, feel free to post them in the comments section below and I'll do my best to answer them for you.

Comments


No comments yet, be the first to leave one.

Leave a Comment