Building Data Adapters [Jason He]

Introduction to Adapters

 

If two types (A and B) have to version independently, they cannot have static references between them. Instead an adapter is needed to sits between A and B. It enables them to communicate without a direct reference and thus providing a versioning boundary.

 

An adapter normally takes one type in the constructor and exposes the methods as another type. Here is an example

    public interface A { void methodA();}

    public interface B { void methodB();}

    public class Adapter : A

    {

        B interfaceB;

        public Adapter(B ib) { interfaceB = ib; }

        public void methodA() { interfaceB.methodB(); }

}

This adapter looks like an A. It actually implements A by calling functions in B. Since A, B and Adapters are normally in different assembly, A and B don’t have a versioning dependency. For example, if we got a newer version B2, we don’t need to update A assembly to reflect the changes. We only need to update adapters to reroute the function calls. Updating Adapters is easier to do than updating Host and AddIn.

 

When to use data adapters

When you define a rich object model for your host and add-ins to communicate over you need to decide how you want to communicate over the isolation boundary. In our model, for each data type that goes across the isolation boundary we need one Contract, two views and two Adapters. Typically those defining the object model and building the pipeline will have one assembly for each pipeline component (Contract, Add-In/Host View, Add-In Side/Host Side Adapter) and group the pieces for all types in their system into that single assembly.

 

Calculator scenario

Let us consider the calculator sample that Jesse and Jack wrote on the MSDN magazine. In that sample, all the types passing between Host and AddIn are primitive types like “string” “double”. What happens if Host has a complex data type that it expects an AddIn to access? Let us assume we want to treat operator and operands as one object and pass it to the calculator AddIn. In this case, our contract becomes a little more complex.

 

We’ll walk through the important code in the sample below, but for the full example in VS Solution form see the attached file.

 

Here is the contract

    [AddInContract]

    public interface ICalculatorContract: IContract

    {

         string GetAvailableOperations();

        double Operate(IOperateContract operate);

    }

    public interface IOperateContract : IContract

    {

        string GetOperation();

        double GetA();

        double GetB();

    }

 

We create an operate contract which exposes all the data that calculator AddIn needs to access. This contract will be passed from Host side to the AddIn side.

 

Here is our AddInBase and AddIn

 

    [AddInBaseAttribute]

    public abstract class Calculator {

       

        public abstract string Operations {

            get;

        }

       

        public abstract double Operate(Operate operate);

    }  

   

    public abstract class Operate {

       

        public abstract string Operation {

            get;

        }

       

        public abstract double A {

            get;

        }

       

        public abstract double B {

            get;

        }

    }

    [AddIn("CalculatorV3AddIn")]

    public class CalulatorV3AddIn : Calculator

    {

        public override string Operations

        {

            get { return "+ - * / **"; }

        }

     public override double Operate(Operate operation)

        {

            switch (operation.Operation)

            {

                case "+":

                    return operation.A + operation.B;

                case "-":

                    return operation.A - operation.B;

                case "*":

                    return operation.A * operation.B;

                case "/":

                    return operation.A / operation.B;

                case "**":

                    return Math.Pow(operation.A, operation.B);

                default:

                    throw new InvalidOperationException("This add-in does not support: " + operation.Operation);

            }

        }

    }

The AddIn code looks quite similar to the old calculator sample. The biggest difference is AddIn now takes an Operate as a parameter. From AddIn side, this is how the Host side operate object looks like. This Operate is defined in AddInBase so that both AddIn and AddInAdapter can use it.

 

Here is the AddInAdapter

 

    [AddInAdapterAttribute]

    public class CalculatorViewToContractAddInAdapter :

ContractBase, ICalculatorContract {

       

        private Calculators.Extensibility.Calculator _view;

       

        public CalculatorViewToContractAddInAdapter(Calculator view) {

            _view = view;

        }

       

        public virtual string GetAvailableOperations() {

            return _view.Operations;

        }

       

        public virtual double Operate(IOperateContract operate) {

            return _view.Operate(new OperateContractToViewAddInAdapter(operate));

        }

       

    }

    public class OperateContractToViewAddInAdapter : Operate {

       

        private IOperateContract _contract;

       

        private System.AddIn.Pipeline.LifetimeTokenHandle _handle;

       

        public OperateContractToViewAddInAdapter(IOperateContract contract) {

            _contract = contract;

            _handle = new System.AddIn.Pipeline.LifetimeTokenHandle(contract);

        }

       

        public override string Operation {

            get {

                return _contract.GetOperation();

            }

        }

       

        public override double A {

            get {

                return _contract.GetA();

            }

        }

       

        public override double B {

            get {

                return _contract.GetB();

            }

        }

       

    }

 

We intentionally created a long type name here OperateContractToViewAddInAdapter. The name explains its role quite well. It is an Adapter. It is an Adapter at AddIn side. It is an Adapter to adapt a contract to a view. When we look at the AddIn code, we may ask the question who created Operate. Now we know that AddInAdapter created an instance of OperateContractToViewAddInAdapter which derives from Operate and passed it to AddIn. The contract we are adapting here is IOperateContract. The next question will be who created IOperateContract at the host side.

 

Let’s take a look at HostAdapter code.

    [HostAdapterAttribute]

    public class CalculatorContractToViewHostAdapter :Calculator {

       

        private ICalculatorContract _contract;

       

        private LifetimeTokenHandle _handle;

       

        public CalculatorContractToViewHostAdapter(ICalculatorContract contract) {

            _contract = contract;

            _handle = new LifetimeTokenHandle(contract);

        }

       

     public override string Operations {

            get {

                return _contract.GetAvailableOperations();

            }

        }

       

        public override double Operate(Operate operate) {

            return _contract.Operate(new OperateViewToContractHostAdapter(operate));

        }

       

    }

    public class OperateViewToContractHostAdapter : ContractBase, IOperateContract {

       

        private Calculators.Extensibility.Operate _view;

       

        public OperateViewToContractHostAdapter(Operate view) {

            _view = view;

        }

       

        public virtual string GetOperation() {

            return _view.Operation;

        }

       

        public virtual double GetA() {

            return _view.A;

        }

       

        public virtual double GetB() {

            return _view.B;

        }

       

    }

 

We got another adapter at host side called OperateViewToContractHostAdapter. It adapts a host side view to a contract. The host side view looks like

 

    public abstract class Calculator {

       

        public abstract string Operations {

            get;

        }

       

        public abstract double Operate(Operate operate);

    }

    public abstract class Operate {

       

        public abstract string Operation {

           get;

        }

       

        public abstract double A {

            get;

        }

       

        public abstract double B {

            get;

        }

    }

 

 

 To expose the operate object, we created 1 contract, 2 views and 2 adapters. It stacks up like below

 

Host

Operate

OperateViewToContractHostAdapter

IOperateContract

OperateContractToViewAddInAdapter

Operate

AddIn

 

As you can see our system for adapting data is same one we use for adapting the add-ins themselves: the only difference is that the system activates the add-ins and their adapters directly, rather than relying on other pieces in the pipeline to do so. We may notice from above AddIn and Host adapters that both adapters can adapt IContract to View and View to IContract. It is symmetric. It is a common misconception that adapters adapt one way not the other inside of a given pipeline component. Sometime, both ViewToContract and ContractToView adapters are required in one assembly if the data type is used as both in and out parameters of a contract.

 

We may also notice that both views and adapters have similar code. It is actually not a crazy idea to merge two views into one assembly and two adapters into one assembly. As long as we put the assemblies at the right location, our AddIn API will be able to discovery and activate the pipeline as expected.

 

In summary, we have illustrated adapters that can link views and IContract at both Host and AddIn side. For each data type that we want to expose from Host to AddIn or from AddIn to Host, we need to define one Contract, two views and two adapters. Adapter is the key to break the versioning dependency.

ExtensibleCalculator.zip