Creating Truly Reliable Plugins Based on the Managed Add-In Framework

Introduction

For the high-quality and fast implementation of diverse customer requirements, we need plugins. Having studied a variety of plugins, we found out that the Managed Add-In Framework (hereinafter MAF) from Microsoft is the best option for us. Practice has demonstrated that we were right—the plugins work, users are satisfied, and customers are happy. However, MAF has one drawback—the lack of documentation and articles describing how to use it. All we have found is poor documentation and a few posts on Stack Overflow. However, we will partially address this gap by describing how to create a plugin from scratch, considering all the nuances of working with MAF in the process.

One of our projects is a document and e-mail management system. It is used by TV companies, lawyers and healthcare centres—basically, those who care about the storage of large amounts of documents safety. Initially, it was intended to be a version control system for documents, but feature by feature, new functionality was added to speed up and simplify work with documents, including the development of templates for documents and document sets, tags, sending files by e-mail, document previews, and so on. With time, users required more and more specific features, and one needed document export capabilities with a date and time stamp. Another wanted to add in documents formatted with a header and footer, and others needed an intuitive way to create a set of documents from a template. If all these features had been implemented, our application would have looked like this:

A browser that has way too many toolbars

To avoid such congestion, it was suggested to our customers to implement specific features as plugins that would be included in the configuration for each customer or organisation. This approach would also allow third-party developers to create new plugins.

After our plugin platform research, MAF was selected because it allows for plugins to be created on the .NET Framework and provides the following required features:

  • Finding: Plugins that are inherited from the contract supported by the host.
  • Activation: Loading, launching, and establishing a connection with the plugin.
  • Isolation: Using app domains or processes to create an isolation boundary that protects the host from possible security problems or unhandled exceptions.
  • Interaction: Provides communication between the host and the plugin across the isolation boundary using method calls and data exchange.
  • Lifetime management: Predictable and easy-to-use loading and unloading of app domains and processes.
  • Versioning: Verification that the host and plugin may communicate when new versions of the host or plugin are created.

The basic element in MAF is the contract, which defines how hosts and plugins communicate. The host uses a host view of the contract, and the plugin uses a plugin view of the contract. For data exchange between the plugin and the host via views, adapters are used:

Isolation boundary

Contracts, views and adapters are segments that make up the plugin pipeline. The pipeline handles the data exchange between the plugin and the host and supports versioning, lifetime management, and isolation.

Quite often, the display of a plugin UI is required in the host application. Because different .NET Framework technologies have different UI implementations, WPF extends the .NET Framework plugin model to support plugin interface displaying.

MAF technology was released in the scope of .NET Framework 3.5 and has not yet been changed at this time. This makes the technology look outdated, and its complexity scares users away. The necessity of implementing five segments of the pipeline seems to be excessive, but it is precisely this approach that provides isolation and versioning.

For example, let’s implement a simple application that displays the file list in a selected folder and add a plugin that will copy the file to the export folder and convert it to PDF.

Beginning

Project and folder structure

To begin, let’s take a look at the plugin’s pipeline structure:

pipeline structure

Host views of add-ins

Contain interfaces or abstract classes that represent host view of the plugin and the types that flow between them.

Contracts

If the host and plugin are loaded into separate app domains, then the contract segment is an isolation boundary between them.

Requirements: All contracts must inherit from the IContract interface. The ContractBase class contains the basic implementation of the IContact. For isolation reasons, contracts must use only other contracts derived from IContract, primitive data types (integers and Boolean types), serialisable types (reference types and types defined in Mscorlib.dll or in the contract assembly), or enumerations (defined in Mscorlib.dll or in the contract assembly).

To construct the pipeline, the contract that represents the add-in must be identified with the AddInContractAttribute attribute.

Add-in views

Interfaces or abstract classes are provided as views for the host, and their types flow between the plugin and the host.

To construct the pipeline, the type in the add-in view that the add-in implements or inherits is identified by the AddInBaseAttribute.

Host-side adapters

Views can be converted to contract and vice versa depending on the direction of the call.

View-to-contract adapters implement the contract by calling into the view passed to its constructor and are marshalled across the boundary as a contract. This class must inherit the ContractBase and implement the contract. Meanwhile, the contract-to-view adapter implements or inherits the view segment that it is converting, depending on whether the view is an interface or an abstract base type, and implements the members of the view by calling into the contract that is passed to the adapter’s constructor.

To construct the pipeline, you must identify the host-side adapter class by applying the HostAdapterAttribute attribute.

Add-in-side adapters

These are the same as host-side adapters, but in contrast, they convert the contract into the plugin view.

To construct the pipeline, it is essential to identify the add-in-side adapter class by applying the AddInAdapterAttribute attribute.

For each pipeline segment, there should be a corresponding project and folder (except the host). Let’s create all necessary projects for a pipeline (project type for all projects should be “Class Library”):

  1. Pipeline.Contracts. Add reference to System.AddIn и System.AddIn.Contract.
  2. Pipeline.AddInViews. Add reference to System.AddIn.
  3. Pipeline.HostViews.
  4. Pipeline.AddInAdapters. Add reference to System.AddIn, System.AddIn.Contract, Pipeline.AddInViews, Pipeline.Contracts. For the last two references, copying to the output folder should be disabled (Copy local = False). Otherwise, MAF will not identify which dll to load, and this segment will not be loaded while constructing the pipeline.
  5. Pipeline.HostAdapters. The same holds for Pipeline.AddInAdapters, but instead of the project Pipeline.AddInViews refers to Pipeline.HostViews.
  6. DemoPlugin (host).

All these library outputs should be mapped into a strict folder structure:

 foldee structure for MAF

The host view can be located anywhere, as it is used by the host.

Configuring the copying of the build results in all projects being assigned to the appropriate directory. The output will be as follows:

 Note: It is important to copy each plugin to a separate folder; otherwise, MAF will not find them Note: It is important to copy each plugin to a separate folder; otherwise, MAF will not find them.

Implementation of host-to-plugin communication

For the host to be able to manage the plugin, we need to create a contract that the plugin will implement. Let’s begin creating an interface with one method that returns the plugin display name:

[AddInBase]
public interface IExportPluginView
{
  string DisplayName { get; }
}

Using MAF interfaces in the host and plugin views might lead to different results, and adapters will adapt them to each other. To make the process easy, let’s use similar interfaces in all pipeline segments. IExportPluginView should be created in Pipeline.AddInViews, Pipeline.Contracts and Pipeline.HostViews projects. The difference will be that the interface in the plugin view must have AddInBaseattribute (to allow MAF to identify this class as the plugin), and the interface in contracts should be inherited from ContractBase.

In turn, adapter classes should be created. First, let’s look at the adapter from the plugin view to the contract:

[AddInAdapter]
public class ExportPluginViewToContractAdapter : ContractBase, Pipeline.Contracts.IExportPlugin
{	
  private readonly Pipeline.AddInViews.IExportPluginView view;
  public string DisplayName => view.DisplayName;
  
  public ExportPluginViewToContractAdapter(Pipeline.AddInViews.IExportPluginView view)
  {
  	this.view = view;
  }
}

As an argument, the constructor takes the corresponding interface if in the plugin view. The AddInAdapter attribute should be applied for this class.

Then, the adaptor can be created from the contract in host view (project Pipeline.HostAdapters):

[HostAdapter]
public class ExportPluginContractToHostAdapter : IExportPluginView
{
  private readonly Contracts.IExportPlugin contract;
  private readonly ContractHandle handle;
  
  public string DisplayName => contract.DisplayName;
  
  public ExportPluginContractToHostAdapter(Contracts.IExportPlugin contract)
  {
  	this.contract = contract;
    handle = new ContractHandle(contract);
  }
}

The HostAdapter attribute is applied for this adapter, and as a constructor argument, it gets an instance of the corresponding contact interface. This handle is managed by the ContractHandle class. Since the plugin and host are usually in separate app domains, standard garbage collection does not work properly. The ContractHandle is used to solve this problem by using build-in tracking tokens for receiving and releasing contract instances.

Plugin activation and integration

To activate the plugin for the first time, the pipeline cache should be updated by specifying the directory where all pipeline libraries are located. In our case, it is the ‘Pipeline’ folder next to the host executable file:

string pipelinePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Pipeline"); 

AddInStore.Update(pipelinePath); 

As a result, MAF creates two files:

  • “Pipeline/PipelineSegments.store” — pipeline segments cache
  • “Pipeline/AddIns/AddIns.store” — plugins cache in AddIns folder.

When the plugin code or pipeline segment’s interfaces/objects change, the corresponding cache file must be deleted to be generated again at the next launch.

If plugins are located in folders other than ‘AddIns’, a cache file should be created for them too:

AddInStore.UpdateAddIns(otherPluginsPath);

In this case, ‘otherPluginsPath’ is a list of paths to plugins. In addition, the UpdateAddIns method is used to update information about plugins if they were added, removed or changed.

Then, all plugins implementing the IExportPlugin interface need to be found:

var addInTokens = AddInStore.FindAddIns(typeof(IExportPluginView), pipelinePath);

The interface type, pipeline directory, and list of all directory paths where plugins may arise should be searched. As a result, a list of AddInToken objects will be returned. The AddInToken class contains basic information about a plugin and has a plugin activation method.

There are several plugin activation options depending on required host isolation:

  1. No isolation: Plugins and the host run in the same process and app domain.

    This isolation gives minimum protection and security: all data is available, there is no protection from unhandled exceptions, and there is no ability to unload problematic code. There is a chance of host failure because of an unhandled exception in the plugin.

  2. Medium isolation: Each plugin runs in its own app domain.

    Using this option, security level and activation configuration can be managed by activation method parameters. Also, this option provides the ability to unload the plugin in case of error, but in some cases, this might not work, so the plugin might still crash the entire application.

  3. High isolation: Each plugin runs in its own process.

    This option gives maximum security for the host because plugin exceptions will not lead to a crash. At the same time, this option requires more complicated implementation of many hosts’ interprocess communication and synchronisation. Passing WPF elements between processes is also more laborious.

For our project, the second option of medium isolation was selected because it provides enough security and flexibility and does not require sophisticated implementation for interprocess communication. For our demo, let us also choose the middle isolation option with a minimum security level.

var plugin = addInTokens.First().Activate<IExportPluginView>(AddInSecurityLevel.FullTrust);

As a result, a plugin class instance will be created. It can be used, for example, to display the name of the activated plugin:

MessageBox.Show($"Plugin '{plugin.DisplayName}' has been activated""MAF Demo message", MessageBoxButton.OK);

Extending functionality

Implementation of backward communication from plugin to host

There are scenarios when a plugin may need data from the host application. For example, it may be necessary to acquire the latest document version or document properties. To do this, a special contract should be created and passed as a parameter into the Initialize plugin method.

For now, let plugin API have one method returning the last modified date and time of the file. Let’s create IPluginApi in pipeline segments: HostView, AddInView and Contracts (do not forget to set the AddInContract attribute and inherit from IContract accordingly—the same as with the IPluginView):

[AddInContract]
public interface IPluginApi : IContract
{
	DateTime GetLastModifiedDate(string path);
}

This step requires an adapter as well as the IPluginView, but in the opposite direction. Correspondingly, at the host-side adapter level, this will be the adapter from the host view to the contact, and on the plugin-side adapter level, this will be the adapter from the contract to the plugin view:

[HostAdapter]
public class PluginApiHostViewToContractAdapter : ContractBase, IPluginApi
{
  private readonly HostViews.IPluginApi view;
  
  public PluginApiHostViewToContractAdapter(HostViews.IPluginApi view)
  {
  	this.view = view;
  }
  
  public DateTime GetLastModifiedDate(string path)
  {
  	return view.GetLastModifiedDate(path);
  }
}

To use these adapters, we need to add the Initialize method, which in turn will be used in the adapters as follows:

Host adapter:

public void Initialize(IPluginApi api)
{
  contract.Initialize(new PluginApiHostViewToContractAdapter(api));
}

Plugin adapter:

public void Initialize(IPluginApi api)
{
  view.Initialize(new PluginApiContractToPluginViewAdapter(api));
}

As a finishing touch, let us implement the Pipeline.HostView.IPluginApi interface on the host side and initialize our plugin with it:

var plugin = addInTokens.First().Activate<IExportPluginView>(AddInSecurityLevel.FullTrust); 

plugin.Initialize(new PluginApi()); 

Adding plugin UI

To display the plugin UI in the host application, MAF has a special INativeHandle interface that holds a window descriptor (Hwnd). After getting the window descriptor from the resources, the INativeHandle is passed between app domains so the host can display the UI element of the plugin.

To demonstrate how it works, let us add a method returning a small plugin panel element, and the host will display it within its window.

In the host and plugin segment method, the return UI panel will be declared as follows:

FrameworkElement GetPanelUI()

However, in the contract segment, the same INativeHandleContract is used to pass across the isolation boundary.

INativeHandleContract GetPanelUI();

To create INativeHandleContract in a plugin-side adapter, a converter integrated in MAF is used:

public INativeHandleContract GetPanelUI()
{
  FrameworkElement frameworkElement = view.GetPanelUI();
  return FrameworkElementAdapters.ViewToContractAdapter(frameworkElement);
}

To get the UI element from the descriptor interface, the inverse converter method is used:

public FrameworkElement GetPanelUI() 
{
  INativeHandleContract handleContract = contract.GetPanelUI();
  return FrameworkElementAdapters.ContractToViewAdapter(handleContract); 
} 

To use the FrameworkElementAdapters class, add a reference to the System.Windows.Presentation library.

Having done all this, the only step that remains is to create a control to be passed to the host application on demand:

public FrameworkElement GetPanelUI() 
{
  return new PanelUI(); 
} 

The host, in turn, can use this UI element as the content of a ContentControl. The result might look like this:

Plugin panel in UI

There are a few nuances using UI elements passed in this manner:

  • Triggers cannot be applied to the plugin element.
  • Plugin elements will always be displayed on top of other elements regardless of ZIndex. However, other application windows (e.g., dialogues and popups) will be displayed correctly (that is why we have to change the implementation of toast notifications using popup windows).
  • Plugin elements will not be displayed at all if windows have opacity or transparency; in addition, the AllowTransparenсy property must be set to false (in our case, the problem with displaying the plugin UI was caused by the WindowChrome.GlassFrameThickness property).
  • The host cannot receive any mouse events or GotFocus and LostFocus events from the plugin element. Also, the IsMouseOver property is always false.
  • VisualBrush cannot be used in a plugin element, and media cannot be played on the MediaElement.
  • Transformations (e.g., rotate, scale, and screw) cannot be applied to the plugin element.
  • The plugin element will not display XAML changes on the fly; instead, you have to build a plugin and refresh the cache file to update the element’s UI.
  • Before plugin shutdown, all dispatchers used in the plugin should be closed manually. To do this, a corresponding method should be implemented in a plugin contract. This will allow the host to signal the plugin before shutdow.

Plugin stylisation

To make the plugin UI look consistent with the host UI, styles for common controls and elements (e.g., buttons, inputs, comboboxes, textblocks, etc.) can be extracted into a separate library that will be used by both the host and the plugin.

To do this, add a new project of type ‘Class library’, create ResourceDictionary items with styles to be shared, and create a ResourceDictionary to merge all these dictionaries together:

AllSTyles.XAML

To use these styles in the plugin, add a reference to the shared styles library and refer to a merged dictionary that includes all resource dictionaries in the XAML file, where styles can be applied:

<UserControl.Resources> 
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries> 
            <ResourceDictionary Source="pack://application:,,,/Demo.Styles;component/AllStyles.xaml"/> 
         </ResourceDictionary.MergedDictionaries> 
    </ResourceDictionary> 
</UserControl.Resources> 

Next, set a proper style for the element using a static or dynamic resource reference or leave it in the default style.

It is important to copy all dependencies and third-party libraries of the shared styles library to the plugin output folder. A post-build event with an xcopy command can be used for this.

Plugin deactivation and shutdown

Sometimes, application usage scenarios might require plugins to be unloaded and others loaded. In the case of our project, this was necessary when changing the connection to the server, because each server had its own set of plugins to be used on the client side.

A plugin unload consists of two steps:

1. Close contract handle.

It is necessary to release references and resources connecting the plugin and host via the pipeline. The necessity of this step was proven by practice, because sometimes plugins would result in an error referring to the impossibility of creating a handle on the contract on the plugin-side adapter when reactivated.

To do this, the plugin-side adapter should contain a method to dispose of the contract handle:

[AddInAdapter] 
public class PluginApiContractToPluginViewAdapter : AddInViews.IPluginApi
{
  public void Unload()
  {
  	handle.Dispose();
  }
} 

To call this method from the host, add it to the IPluginHostView interface as well.

2. Shut down plugin

A plugin is shut down with its controller. To get to the controller, use a plugin object instance:

var controller = AddInController.GetAddInController(plugin); 

controller.Shutdown();  

The shutdown method terminates the connection between the host and the plugin through the pipeline. Also, the plugin app domain is unloaded if it was created on the plugin activation. If the plugin was activated in the scope of the host app domain after the shutdown method, the pipeline segments will no longer have references to the plugin instance.

Conclusion

At first glance, MAF might seem too bulky and may require a lot of repetitive code. However, our practice has shown how a developer can get used to implementing new functionality in all pipeline segments, and it no longer may seem so difficult. Well-written adapters with helper and converter methods and classes can allow for new functions to be easily and quickly added and existing ones extended in the future.

In general, this technology works stably and reliably. We have seven developed plugins that are constantly used by clients, and none have resulted in application crashes noticeable to the user.

P. S. The complete code for the demo application can be found on github.

You Might Also Like

Blog Posts Developing SQL Query Testing System. Part 2
October 21, 2021
We developed a data layer testing framework to automate and simplify the process of testing complex SQL queries on a large project.
Blog Posts Techniques for Handling Service Failures in Microservice Architectures
October 13, 2021
This article may be useful for those who have suffered from the instability of external APIs: what are the strategies for handling failures and which way we found to deal with the problem.
Blog Posts Secure Web Application Cheat Sheet
October 08, 2021
This article is intended as a cheat sheet for web developers. It describes some basic steps and measures to create a secure web application protected from the most widely spread threats.