转贴 MAF Step by Step

来源:互联网 发布:算法豆瓣 编辑:程序博客网 时间:2024/05/16 23:40
Creating Extensible Applications with MAF (System.AddIn)


Last month, my colleague, Pinku Surana,wrote an article about .NET AppDomains and how they can be used to providecomponent isolation and make your applications more reliable.  If you missed it, you can read the Developmentsarchives on Developmentor’s website.   This month, I’d like to continue exploring reliabilityand extensibility by introducing you to a new framework included with .NET 3.5:The Managed Add-in Framework (MAF), sometimes referred to as System.AddIn.

Developers (and managers) have long desired a way to easilycreate extensible applications that allow new features to be added withoutjeopardizing the stability of the existing code base.  The .NET framework has provided the underlying support toaccomplish this from the very beginning through the reflection API andAppDomain support Pinku examined last month.  MAF builds on that fundamental support to provide ahigher-level service that allow you to dynamically discover, load, secure and interactwith external assemblies used to provide features for your application.  Several common architecturalrequirements are made trivial with MAF:

1)  Isolate aspects of your code for securityreasons or partial-trust scenarios.

2)  Allow business partners to extend yourapplication safely without access to the source code; Adobe Illustrator is agreat example of this style of application.

3)  Separate volatile sections of your applicationout – where depending on the customer the application needs to executedifferent sets of code.

4)  Add or change code without unloading theapplication – for example pay-to-play scenarios, or where you need toupdate an assembly but the application must continue to run.

5)  Develop and evolve different sections of theapplication in parallel without any fear of destabilizing one based on theother.

If any of these scenarios sound like something you couldutilize in your application then you should know about MAF!

At the center of the MAF-based application is the pipeline.

The Pipeline

Communication between add-ins and the host application isstrictly regulated by the pipeline. The pipeline is dynamically created by MAF through a set ofloosely-coupled components which are used to version, marshal and transform thedata as it passes back and forth between the host and each loaded add-in.

image001.png

Each section of the pipeline is contained in a separateassembly, loaded as necessary to manage the specific add-in.  MAF discovers each component and thenloads them on request using reflection.  

For reliability, MAF allows optional isolation boundaries tobe created between the add-in and host – everything to the left of thecontract is loaded into the main (host) AppDomain and everything to the rightloaded into a newly created AppDomain which has its own security permission set.  The isolation can also be done throughcross-process calls if true process-level isolation is necessary.  Under the covers, the system usestraditional remoting calls to do the work of marshaling calls back and forth.

Breaking down the pipeline, there are three main partsstarting in the center with the contract.

Contract

As you might expect, MAF is based on interfacecontracts.  Interfaces allow classesin an application to be loosely coupled, reducing the risk when changes aremade between dependent sections of the code.   The interface contract is shared between the host andadd-in and once it has been established, it should never be changed. 

Consider the simple example of a translator program.  The host will expect to load one ormore translator add-ins that will do some work on an inbound string and returnthe results.  To accomplish this, Imight create an ITranslatorinterface that looks like:

[AddInContract]

public interface ITranslator : IContract

{

  string Translate(string input);   
}


Notice that the interface derives from IContract.  This is a requirement foradd-in contracts and is what will be used to provide marshalling support whenthe pipeline is established.  Wealso need to decorate the interface with the [AddInContract] attribute – this is the marker used by MAF toidentify the contract when it is dynamically constructing the pipeline.  Both of these types come from the System.AddIn.Pipeline namespace in System.AddIn.Contract.dll.

Views

Moving to the edges, we find the views.  This is the code that the add-in andhost directly interact with.  Itrepresents a host or add-in specific “view” of the contract and, like thecontract, is contained in a separate assembly.

Both of the view classes will echo the structure of thecontract, but not actually be dependent upon the contract.  For example, our host side view mightlook like this:

public abstract class TranslatorHostView

{

  public abstract string Translate(string input);
}


The view is commonly exposed as an abstract class to make it look more naturalwhen used by the host, but an interface will work just as well.  The host uses the view directly whencommunicating with any add-in based on the contract.

On the add-in side, we have an almost identical class– except we decorate this type with the [AddInBase] attribute so MAF knows which side of the pipeline thisview is for (the add-in).   Thisis located in the System.AddIn.Pipelinenamespace in the System.AddInassembly.

When we create each add-in, the implementation will use thistype as the base class.

[AddInBase]

public abstract class TranslatorAddInView

{

  public abstract string Translate(stringinp);
}

Adapters

The last piece of the pipeline is the adapters.  The adapters play a very special rolein the pipeline – they are the glue that binds the contract to the view:implementing lifetime management and any necessary data conversion between thetwo ends. 

It may seem redundant to have this class, but by separatingthe view from the contract we introduce version tolerance into our architecture– the host can version independently of the add-in and vice-versa.  Depending on the situation, theadapters can be as simple as a pass-through class, or can provide higher-levelservices to allow non-serializable types to be marshaled across the isolationboundaries.

On the host side, the adapter will implement the view(remember it is either an interface or abstract class).  It will be passed a reference to thecontract in the constructor and it is responsible for connecting the two together.

[HostAdapter]

public class TranslatorHostViewToContract :  

                    TranslatorHostView

{

  ITranslator _contract;

  ContractHandle _lifetime;

 

  public TranslatorHostViewToContract(

             ITranslator contract)  

  {

     _contract =contract;

      _lifetime = new ContractHandle(contract);

  }

 

  public override string Translate(string inp) 

  {

     return_contract.Translate(inp);
   }
}


In this simple example, the code caches off the contract interface.  This is a remoting proxy to the actualloaded add-in.  The adapter thenimplements the Translate method andpasses it forward to the contract for implementation.  If we had non-serializable types, then the adapter would beresponsible for converting them into something that was serializable. 

It also provides some lifetime management for thecontract.  Because the contract isa remoting proxy and is likely running in a separate AppDomain (or evenprocess) we have to be concerned with how long it lives.  MAF provides all the support for this throughthe ContractHandle class.  Most of the time, all you will need todo is store a ContractHandle in yourhost adapter and then pass it the inbound contract to wrap in your constructor.

Finally, in order for MAF to identify this class, it must bedecorated with the [HostAdapter]attribute from the System.AddIn.Pipelinenamespace out of the System.AddIn.dll.

The add-in side looks very similar, but does the opposite: itmakes the add-in view look like the contract.

[AddInAdapter]

public class TranslatorAddInViewToContract :

                 ContractBase, ITranslator

{

  TranslatorAddInView _view;

 

  public TranslatorAddInViewToContract(

            TranslatorView view)  

  {

     _view = view;

  }

 

  public string Translate(string inp)  { 

      return _view.Translate(inp); 

  }

}

 
MAF passes the view to the constructor and the class caches the reference offin a field.  It implements the contract(ITranslator) and ContractBase which provides theimplementation of the IContract interfacefor us (remember this was a required interface on our contract).  As the host makes calls to the contractinterface, this class will translate those calls to the add-in view, which asyou will see is the implementation provided by the add-in itself.  Note how the [AddInAdapter] attribute is used to mark this class so MAF candiscover it.

If it seems like all the above is a lot of repetitious,boilerplate code well.. you’re right! To make it easier on the developer to create the pipeline components,the MAF team has created a pipelinebuilder available at http://www.codeplex.com/clraddins.  It takes the contract assembly and thengenerates the views and adapters from it:

image003.jpg

Creating an add-in

Once the pipeline pieces are built you can createadd-ins.  Each add-in provides animplementation of the abstract add-in view.  For example, we might provide a BabelFish add-in foruniversalized translation:

[AddIn(“BabelFishTranslator”,

       Description=“Universaltranslator”,

      Version=“1.0.0.0”,Publisher=“Zaphod Beeblebrox”]

public class BabelFishAddIn :TranslatorAddInView

{

  public string Translate(string input)

  {

     ...

  }
}


The add-in implements the AddInView,providing a concrete implementation of the Translatemethod.  It is decorated with the [AddIn] attribute that allows it toprovide a name, version, description and other data the host can use to decidewhether it is a useful translator.

Putting the pieces together – the directory structure

To properly identify each of the components necessary, MAFenforces a particular directory structure you need to follow fordeployment.  Each piece is storedin a sub-directory off the root of the pipeline directory (this is typicallythe APPBASE where the host executable is stored). 

image006.png

Thedirectory names are required, but not case sensitive – each directoryholding a single piece of the pipeline that MAF will load dynamically when itis loading the add-in.  Thehost-side view is always located in the same directory as the host executableitself so it does not need a dedicated directory.  When creating your Visual Studio project, it is important toset the output directories appropriately so that you create the above directorystructure.  In addition, allreferences between the components should be marked as CopyLocal = “false” inVisual Studio to ensure a local copy of the assembly isn’t placed into thesub-directory: 

image008.png

Discovering and loading add-ins from the host

The last piece of the puzzle is the actual loading ofadd-ins from the host.  This isdone in three basic steps:

1.    Identify and catalog the add-ins

2.    Retrieve the list of specific add-ins based onview or name

3.    Activate and use the add-in

The first step is to identify the available add-ins for thehost.  This is done through the System.AddIn.AddInStore class:

string[] errorList = AddInStore.Rebuild(

        PipelineStoreLocation.ApplicationBase);

 

Calling Rebuildforces MAF to walk the directory structure and create the pipelinedatabase.  It stores thisinformation in the pipeline root directory and returns a list of errors, if anyoccur.  The most common errors aremissing pipeline components – where MAF is unable to locate some requiredportion of the pipeline such as a View or Adapter.

Next, the host will retrieve a list of add-ins based on thehost view through the FindAddInsmethod – these will be the add-ins conforming to a specific contract(whatever the view/adapter is bound to):

Collection<AddInToken> addinTokens =AddInStore.FindAddIns(

               typeof(TranslatorHostView),

               PipelineStoreLocation.ApplicationBase);

 

The first parameter is the host view type – so MAFknows what add-ins we are looking for, the second is the pipeline rootdirectory, which is the same directory passed to the Rebuild method and indicates where the pipeline database is stored.

 

The returning collection represents a series of tokens that are used to identify andcontrol the add-ins.  This is howthe host can examine, activate and unload the add-in.  To activate a specific add-in, you can take the token andcall Activate:

 

Collection<AddInToken>addinTokens;

    ...

foreach(AddInToken token in addinTokens)
{

   TranslatorHostView view =

      token.Activate<TranslatorHostView>(

            AddInSecurityLevel.Internet);

  

   string hello =view.Translate(“Bonjour”);

     ...
}

 

The Activatecall loads all the required components, instantiates the add-in type and returnsthe host view used to interact with the add-in.  Calls made to this object will be marshaled back and forthto the add-in using the pipeline.

 

Notice that the parameter passed to Activate indicates the security level required.  There are several overrides that allowyou to dictate exactly how the add-in is loaded.  You can load the add-in into a specific AppDomain –the current one for best performance:

 

token.Activate<TranslatorHostView>(AppDomain.CurrentDomain)

 

You can specify a specific permission set to restrict thethings the add-in can do on your behalf:

PermissionSetpset = ...;

token.Activate<TranslatorHostView>(pset);


Or you can specify a known permission set based on the CAS zones:

image010.png

Once the add-in is activated, the host can call it just likeany other object – but never forget that you are likely crossing anisolation barrier!  Make sure to designyour contracts so that you minimize the number of calls between the host andadd-ins to ensure your performance doesn’t suffer.

When you are finished with the add-in, you can tell MAF tounload it through the AddInControllerassociated with the view:

AddInController ctrl =    

      AddInController.GetAddInController(view);

ctrl.Shutdown();


This will unload the add-in side pipeline and then destroy the containingAppDomain so you release the resources associated with it.

There are many other capabilities MAF provides such asversioning, passing collections and WPF visuals, passing non-serializabletypes, etc.  You can get a prettydecent overview of the features from MSDN and theMAF team has a blog at http://blogs.msdn.com/clraddins/that has some great, practical information on using this new framework.