Microsoft's add-in framework and the need for diligence

We've recently put Microsoft's managed add-in framework (part of .NET 3.5) into very effective use building a plug-in system for a large asp.net application at work. Essentially the framework in place allows other developers (and our own team for out of stream releases) to develop new functionality for our platform that runs the entire life-cycle for a given widget. In our case for this particular widget we're talking about plugins being responsible for up to 4 asp.net controls in different contexts (for example data collection and reporting as two separate controls) as well as a script injection point where plug-ins are able to extend the scriptability of our platform. 
For us going with the framework gave us a few things we didn't have with our original design for the add-ins. 
  1. Tools to help enforce the pattern
  2. An extra layer of versioning over the somewhat naive approach we started with
  3. Built in discovery, provisioning, and a communication pipeline for serializing types and calls across the contracts that make up the interface between host and plugin
  4. And last but not least support from Microsoft. This is somewhat more minor than the points above, but it helps legitimize our design when we are following the best practices laid out by Microsoft and used by others in similar situations. The documentation and training available also make getting other developers up to speed on the framework that much easier.
There have been numerous challenges in using the framework, but perhaps the most surprising of all for me was the human element and how simple it became over the life of the project to break the pattern by coupling components across or outside the pipeline.

Examples :
  1. Referencing an assembly from both the addin and the host that shared code that should have been passed across the pipeline. 
  2. Bypassing the pipeline completely by calling web services from the addin code (client side or server side calling code) 
  3. Conditional code in the host making decisions based on the type of the addin
  4. Loose coupling based on common knowledge (that shouldn't be common) 
These all basically come down to a breach of contract or an absence of contract for various operations that we needed addins to handle. On some level all of these things can be excused and safely done without compromising the framework if they are done right. It's a slippery slope though and requires a commitment to not be lazy to avoid the temptation to sidestep the pipeline.

In the case of #1 above the shared assembly started off very benign. Essentially some shared utility code for handling urls and some common resource tasks. Why rewrite when that code already existing the main project? Break it off from the project so that it has no dependencies then drop it in. Except that slowly the terrible pain of building contracts, views and adapters for every little interface or interface change drives you towards shortcuts. "Oh I'll just put this code here to test and then fix it later"  Even worse are those cases where you've chosen the path of least resistence in dealing with a bug resulting from unexpected behavior with serialization across the pipeline. It only took a few weeks of not being completely on top of this before I discovered our project was littered with types that were being shared directly between host and addin. Any change meant a recompilation of both projects, completely defeating the purpose.

#2 is a legitimate need in our scenario, and we've found ourselves needing to creating proxy services that wrap our own services just to protect against the inevitable change that will follow. Given that third party developers may be writing code for the platform we have to make an effort to protect from change in all of our interfaces, web service or otherwise. In retrospect I think it would have made more sense to strictly enforce a team division so that no one writing addin code was also writing host code.This probably would have gone a long way to preventing these types of problems.

#3 and #4 are a little more insidious and harder to spot without strict code review. #3 for us isn't technically breaking anything in terms of the interface or future versioning, but adds cruft and generally points to a missing method or property on the interface. The last thing you need as the host is to have case statements littered throughout your code looking for addins. #4 took many forms, and in some cases it's fine. An ok example might be sharing enums, which provided they are defined in the contracts or slightly worse something like a utility class is ok. A not ok example for me was code like this :  extension.GetSetting("Menu_Text");  which in this case has two errors. One "GetSetting" shouldn't really exist because how an addin chooses to configure itself should be transparent to the host. Second this code depends on the addin having a value defined in it's config file for the key "Menu_Text". This is next to impossible to enforce and can of course easily break.

Replacing this with extension.MenuText; should be trivial, and a no-brainer. When we started using the framework back in December we were rolling the supporting code by hand. To give you a sense of what this entails, this is how you would define an extension who's only job is to return MenuText as in the code above :

IExtensionContract.cs
using System.AddIn.Pipeline;
using System.AddIn.Contract;

namespace SimpleExtensionContracts
{
 [AddInContract]
 public interface ExtensionContract : IContract
 {
  string MenuText { get; set; }
 }
}

IExtension.cs
namespace SimpleExtensionContracts.AddInViews
{
    
    [System.AddIn.Pipeline.AddInBaseAttribute()]
    public interface IExtension
    {
        string MenuText
        {
            get;
            set;
        }
    }
}

IExtension.cs
namespace SimpleExtensionContracts.HostViews
{
    
    public interface IExtension
    {
        string MenuText
        {
            get;
            set;
        }
    }
}

IExtensionContractToViewHostAdapter.cs
namespace SimpleExtensionContracts.HostSideAdapters
{
    
    [System.AddIn.Pipeline.HostAdapterAttribute()]
    public class IExtensionContractToViewHostAdapter : SimpleExtensionContracts.HostViews.IExtension
    {
        private SimpleExtensionContracts.ExtensionContract _contract;
        private System.AddIn.Pipeline.ContractHandle _handle;
        static IExtensionContractToViewHostAdapter()
        {
        }
        public IExtensionContractToViewHostAdapter(SimpleExtensionContracts.ExtensionContract contract)
        {
            _contract = contract;
            _handle = new System.AddIn.Pipeline.ContractHandle(contract);
        }
        public string MenuText
        {
            get
            {
                return _contract.MenuText;
            }
            set
            {
                _contract.MenuText = value;
            }
        }
        internal SimpleExtensionContracts.ExtensionContract GetSourceContract()
        {
            return _contract;
        }
    }
}

IExtensionHostAdapter.cs
namespace SimpleExtensionContracts.HostSideAdapters
{
    
    public class IExtensionHostAdapter
    {
        internal static SimpleExtensionContracts.HostViews.IExtension ContractToViewAdapter(SimpleExtensionContracts.ExtensionContract contract)
        {
            if (((System.Runtime.Remoting.RemotingServices.IsObjectOutOfAppDomain(contract) != true) 
                        && contract.GetType().Equals(typeof(IExtensionViewToContractHostAdapter))))
            {
                return ((IExtensionViewToContractHostAdapter)(contract)).GetSourceView();
            }
            else
            {
                return new IExtensionContractToViewHostAdapter(contract);
            }
        }
        internal static SimpleExtensionContracts.ExtensionContract ViewToContractAdapter(SimpleExtensionContracts.HostViews.IExtension view)
        {
            if (view.GetType().Equals(typeof(IExtensionContractToViewHostAdapter)))
            {
                return ((IExtensionContractToViewHostAdapter)(view)).GetSourceContract();
            }
            else
            {
                return new IExtensionViewToContractHostAdapter(view);
            }
        }
    }
}

IExtensionViewToContractHostAdapter.cs
namespace SimpleExtensionContracts.HostSideAdapters
{
    
    public class IExtensionViewToContractHostAdapter : System.AddIn.Pipeline.ContractBase, SimpleExtensionContracts.ExtensionContract
    {
        private SimpleExtensionContracts.HostViews.IExtension _view;
        public IExtensionViewToContractHostAdapter(SimpleExtensionContracts.HostViews.IExtension view)
        {
            _view = view;
        }
        public string MenuText
        {
            get
            {
                return _view.MenuText;
            }
            set
            {
                _view.MenuText = value;
            }
        }
        internal SimpleExtensionContracts.HostViews.IExtension GetSourceView()
        {
            return _view;
        }
    }
}

IExtensionAddInAdapter.cs
namespace SimpleExtensionContracts.AddInSideAdapters
{
    
    public class IExtensionAddInAdapter
    {
        internal static SimpleExtensionContracts.AddInViews.IExtension ContractToViewAdapter(SimpleExtensionContracts.ExtensionContract contract)
        {
            if (((System.Runtime.Remoting.RemotingServices.IsObjectOutOfAppDomain(contract) != true) 
                        && contract.GetType().Equals(typeof(IExtensionViewToContractAddInAdapter))))
            {
                return ((IExtensionViewToContractAddInAdapter)(contract)).GetSourceView();
            }
            else
            {
                return new IExtensionContractToViewAddInAdapter(contract);
            }
        }
        internal static SimpleExtensionContracts.ExtensionContract ViewToContractAdapter(SimpleExtensionContracts.AddInViews.IExtension view)
        {
            if (view.GetType().Equals(typeof(IExtensionContractToViewAddInAdapter)))
            {
                return ((IExtensionContractToViewAddInAdapter)(view)).GetSourceContract();
            }
            else
            {
                return new IExtensionViewToContractAddInAdapter(view);
            }
        }
    }
}

IExtensionContractToViewAddInAdapter.cs
namespace SimpleExtensionContracts.AddInSideAdapters
{
    
    public class IExtensionContractToViewAddInAdapter : SimpleExtensionContracts.AddInViews.IExtension
    {
        private SimpleExtensionContracts.ExtensionContract _contract;
        private System.AddIn.Pipeline.ContractHandle _handle;
        static IExtensionContractToViewAddInAdapter()
        {
        }
        public IExtensionContractToViewAddInAdapter(SimpleExtensionContracts.ExtensionContract contract)
        {
            _contract = contract;
            _handle = new System.AddIn.Pipeline.ContractHandle(contract);
        }
        public string MenuText
        {
            get
            {
                return _contract.MenuText;
            }
            set
            {
                _contract.MenuText = value;
            }
        }
        internal SimpleExtensionContracts.ExtensionContract GetSourceContract()
        {
            return _contract;
        }
    }
}

IExtensionViewToContractAddInAdapter.cs
namespace SimpleExtensionContracts.AddInSideAdapters
{
    
    [System.AddIn.Pipeline.AddInAdapterAttribute()]
    public class IExtensionViewToContractAddInAdapter : System.AddIn.Pipeline.ContractBase, SimpleExtensionContracts.ExtensionContract
    {
        private SimpleExtensionContracts.AddInViews.IExtension _view;
        public IExtensionViewToContractAddInAdapter(SimpleExtensionContracts.AddInViews.IExtension view)
        {
            _view = view;
        }
        public string MenuText
        {
            get
            {
                return _view.MenuText;
            }
            set
            {
                _view.MenuText = value;
            }
        }
        internal SimpleExtensionContracts.AddInViews.IExtension GetSourceView()
        {
            return _view;
        }
    }
}

Yeah, seriously. One interface and one string accessor requires nine class/interfaces and over 200 lines of code (which obviously could be made less with formatting etc).  It's also possible to share the views between addin and host but then you lose part of the more compelling robustness of the framework. If you are interested in where these classes come into play and how the add-in framework actually works check out this link for a good description.
Anyway, I can sympathize with the developers in wanting to speed up the process a bit, but the answer is not to bypass the pipeline. The answer is code generation! Thankfully by the time we realized our mistake Microsoft had released a CTP of their pipeline generator which is a nifty little visual studio addin which picks up the output of the Contracts project and uses reflection to find all of the contracts and generate the necessary projects and files for the pipeline. It literally saved us tons of hours and made the addin framework actually usable. Of couse the code generation is only going to work until we version one side or the other, but at that point we should have solidified those interfaces considerably so it will matter a lot less.

Anyway, long story short, the add-in framework is great, but it's really important for the entire team to understand the goal and be diligent in ensuring that all that extra framework code isn't just being wasted by introducing dependencies.

1 comment:

  1. Thank you, an excellent blog post which has confirmed some of my suspicions about the framework but has provided reassurances too.

    ReplyDelete