August 2013

Volume 28 Number 8

WPF - Architecture for Hosting Third-Party .NET Plug-Ins

By Gennady Slobodsky

Last November Bloomberg L.P. released App Portal, an application platform that allows independent third-party software developers to sell their Microsoft .NET Framework Windows Presentation Foundation (WPF)-based applications to more than 300,000 users of the Bloomberg Professional service.

In this article we’ll present a general-purpose architecture similar to the one employed by the Bloomberg App Portal for hosting third-party “untrusted” .NET applications. The accompanying source code (msdn.com/magazine/msdnmag0813) contains a reference implementation of a .NET plug-in host and a demo plug-in using the Bloomberg API to chart historical pricing information for a given security.

Plug-in Host Architecture

The architecture presented in Figure1 consists of a main application process and a plug-in hosting process.

Architecture for Hosting .NET Plug-Ins
Figure 1 Architecture for Hosting .NET Plug-Ins

Developers implementing a plug-in hosting infrastructure should carefully consider the pros and cons of implementing a plug-in host as a separate process. In the case of the App Portal scenario, we believe the pros of this approach significantly outweigh the cons, but we’ll list the most important factors for your own consideration.

The pros of implementing a plug-in host as a separate process include:

  • It decouples the main application process from plug-ins and, as a result, reduces the possibility of any negative impact plug-ins can have on the performance or usability of the application. It lessens the risk of plug-ins blocking the main application’s UI thread. Also, it’s less likely to cause memory or any other critical resource leaks in the main process. This approach also reduces the possibility for a poorly written plug-in to bring down the main application process by causing either “managed” or “unmanaged” unhandled exceptions.
  • It can potentially improve security of the entire solution by applying sandboxing technologies simi­lar to ones used by The Chromium Projects (see bit.ly/k4V3wq for details).
  • It leaves more virtual memory space available for the main application process (this is more important for 32-bit processes, which are limited by 2GB of virtual memory space available for process user-mode code).
  • It lets you extend functionality of non-.NET applications using .NET plug-ins.

The cons are mostly related to an increase of overall implementation complexity:

  • You need to implement a separate inter-process communication (IPC) mechanism (special attention should be paid to the versioning of the IPC interface when the main application process and the plug-in host have different release or deployment cycles).
  • You must manage the lifetime of the plug-in hosting process.         

Addressing user security is one of the major concerns when designing the hosting process for untrusted third-party plug-ins. Defining a proper security architecture is worth a separate conversation and beyond the scope of this article.

The .NET application domain (System.AppDomain class) provides a comprehensive and robust solution for hosting .NET plug-ins.

An AppDomain has the following powerful features:

  • Type-safe objects in one AppDomain can’t directly access objects in another AppDomain, allowing the host to enforce isolation of one plug-in from another.
  • An AppDomain can be individually configured, allowing the host to fine-tune the AppDomain for different types of plug-ins by providing different configuration settings.
  • An AppDomain can be unloaded, allowing the host to unload plug-ins and all associated assemblies, with the exception of assemblies loaded as domain-neutral (using the loader-optimization options LoaderOptimization.Multi­Domain or LoaderOptimization.MultiDomainHost). This feature makes the hosting process more robust by allowing the host to unload plug-ins that fail in the managed code.

The main application and plug-in host processes can interface using one of the various IPC mechanisms available, such as COM, named pipes, Windows Communication Foundation (WCF) and so on. In our proposed architecture, the role of the main application process is to manage creation of a composite UI and provide various application services for plug-ins. Figure2 shows the Bloomberg Launchpad view, representing such a composite UI. A “Stealth Analytics” component is created and rendered by a WPF-based plug-in hosted by the Bloomberg App Portal, while all other components are created and rendered by a Win32-based Bloomberg Terminal application. The main application process sends commands to a plug-in hosting process through a plug-in Controller Proxy.

An Example Composite UI
Figure 2 An Example Composite UI

A Plug-in Controller is running in the Default AppDomain of the plug-in host process and is responsible for processing commands received from the main application process, loading plug-ins into dedicated AppDomains and managing their lifetimes.

The sample source code provides a reference implementation of our architecture and consists of the hosting infrastructure and the SAPP and DEMO plug-ins.

Application Directory Structure

As Figure 3 shows, the base application directory contains three assemblies:

  • Main.exe represents the main application process and provides the UI for launching plug-ins.
  • PluginHost.exe represents the plug-in hosting process.
  • Hosting.dll contains the PluginController, responsible for instantiating plug-ins and managing their lifetimes.

The Base Application Directory Structure
Figure 3 The Base Application Directory Structure

API assemblies provided for use by the plug-ins are deployed into a separate subdirectory called PAC, which stands for Private Assembly Cache, a concept similar to the .NET Global Assembly Cache (GAC) but which, as its name suggests, holds items that are private to the application.

Each plug-in is deployed into its own subdirectory under the Plugins folder. The name of the folder corresponds to the plug-in’s four-letter mnemonic used to launch it from the UI command line. The reference implementation contains two plug-ins. The first one, associated with mnemonic SAPP, is an empty WPF UserControl that simply prints its name. The second one, associated with mnemonic DEMO, shows a price history chart for a given security using the Bloomberg Desktop API (DAPI).

Inside each plug-in subdirectory there’s a Metadata.xml file as well as one or more .NET assemblies. SAPP’s Metadata.xml contains the plug-in’s Title (used as the window title for the plug-in) and the names of the plug-in MainAssembly and MainClass, implementing the plug-in’s entry point:

<?xml version="1.0" encoding="utf-8" ?>
<Plugin>
  <Title>Simple App</Titlte>
  <MainAssembly>SimpleApp</MainAssembly>
  <MainClass>SimpleApp.Main</MainClass>
</Plugin>

Launching Plug-Ins

A plug-in hosting process creates a single instance of PluginController in the default AppDomain at startup time. The application’s main process uses .NET Remoting to call the PluginController.Launch(string[] args) method to launch the plug-in associated with the mnemonic entered by the user (SAPP or DEMO in the sample reference implementation). The PluginController instance must override the InitializeLifetimeService method inherited from System.MarshalByRefObject to extend its own lifetime, otherwise the object will be destroyed after five minutes (the default lifetime of the MarshalByRefObject):

public override object InitializeLifetimeService()
{
  return null;
}
Public class PluginController : MarshalByRefObject
{
  // ...
  public void Launch(string commandLine)
  {
    // ...
  }
}

The PluginController sets the base directory for the new App­Domain as per the directory structure shown in Figure 3:

var appPath = Path.Combine(_appsRoot, mnemonic);
var setup = new AppDomainSetup {ApplicationBase = appPath};

Using a different base directory for the App­Domain has the following important ramifications:

  • It improves the isolation of plug-ins from each other.
  • It simplifies the development process by using the location of the plug-in’s main assembly as a base directory in the same way as a standalone .NET application.
  • It requires special hosting infrastructure logic to locate and load infrastructure and PAC assemblies located outside the plug-in’s base directory.

When launching a plug-in, we first create a new plug-in hosting AppDomain:

var domain =
    AppDomain.CreateDomain(
    mnemonic, null, setup);

Next, we load the Hosting.dll into the newly created AppDomain, create an instance of the PluginContainer class and call the Launch method to instan­tiate the plug-in.

Seemingly, the easiest way to accomplish these tasks would be to use the AppDomain.CreateInstanceFromAndUnwrap method, because this approach allows you to directly specify the location of the assembly. However, using this approach will cause Hosting.dll to be loaded into the load-from context instead of the default context. Using the load-from context will have a number of subtle side effects, such as the inability to use native images or load assemblies as domain-neutral. Increased plug-in startup time will be the most obvious negative result of using the load-from context. More information about assembly load contexts can be found on the MSDN Library page, “Best Practices for Assembly Loading,” at bit.ly/2Kwz8u.

A much better approach is to use AppDomain.CreateInstanceAndUnwrap and specify the location of the Hosting.dll and dependent assemblies in the XML configuration information of the AppDomain using the <codeBase> element. In the reference implementation, we dynamically generate configuration XML and assign it to the new AppDomain using the AppDomainSetup.SetConfigurationBytes method. An example of the generated XML is shown in Figure 4.

Figure 4 An Example of Generated AppDomain Configuration

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity
          name="PluginHost.Hosting"
          publicKeyToken="537053e4e27e3679" culture="neutral"/>
        <codeBase version="1.0.0.0" href="Hosting.dll"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Bloomberglp.Blpapi"
          publicKeyToken="ec3efa8c033c2bc5" culture="neutral"/>
        <codeBase version="3.6.1.0" href="PAC/Bloomberglp.Blpapi.dll"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WPFToolkit"
          publicKeyToken="51f5d93763bdb58e" culture="neutral"/>
        <codeBase version="3.5.40128.4" href="PAC/WPFToolkit.dll"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

The PluginContainer class derived from System.MarshalBy­RefObject doesn’t need to override default lifetime management as in the case of the PluginController class, because it handles only a single remote call (the Launch method) immediately after creation:

var host = (PluginContainer) domain.CreateInstanceAndUnwrap(
  pluginContType.Assembly.FullName, pluginContType.FullName);
host.Launch(args);

The Launch method of the PluginContainer class creates a UI thread for the plug-in and sets the COM apartment state to single-threaded apartment (STA) as WPF requires:

[SecurityCritical]
public void Launch(string[] args)
{
  _args = args;
  var thread = new Thread(Run);
  thread.TrySetApartmentState(ApartmentState.STA);
  thread.Start();
}

The Run method of the PluginContainer class (see Figure 5) is the startup method of the plug-in’s UI thread. It extracts names of the plug-in’s main assembly and the main class specified by the MainAssembly, and the MainClass elements of the Metadata.xml file, loads the main assembly, and uses reflection to find the entry point in the main class.

Figure 5 The Run Method of the PluginContainer Class

private void Run()
{
  var metadata = new XPathDocument(
    Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 
      "Metadata.xml"))
    .CreateNavigator().SelectSingleNode("/Plugin");
  Debug.Assert(metadata != null);
  var mainAssembly = (string) metadata.Evaluate("string(MainAssembly)");
  var mainClass = (string) metadata.Evaluate("string(MainClass)");
  var title = (string) metadata.Evaluate("string(Title)");
  Debug.Assert(!string.IsNullOrEmpty(mainAssembly));
  Debug.Assert(!string.IsNullOrEmpty(mainClass));
  Debug.Assert(!string.IsNullOrEmpty(title));
  var rootElement = ((Func<string[], UIElement>) 
    Delegate.CreateDelegate(
    typeof (Func<string[], UIElement>),
    Assembly.Load(mainAssembly).GetType(mainClass),
    "CreateRootElement"))(_args);
  var window =
    new Window
    {
      SizeToContent = SizeToContent.WidthAndHeight,
      Title = title,
      Content = rootElement
    };
  new Application().Run(window);
  AppDomain.Unload(AppDomain.CurrentDomain);
}

In the reference implementation, the entry point is defined as a public static method of the main class named CreateRootElement, accepting an array of strings as a startup argument and returning an instance of System.Windows.UIElement.

After calling the entry point method, we wrap its return value in a WPF window object and launch the plug-in. The Run method of the System.Windows.Application class, shown in Figure 5, enters a message loop and doesn’t return until the plug-in’s main window is closed. After that, we schedule unloading of the plug-in’s AppDomain and clean up all resources it was using.

The DEMO Plug-In

The DEMO plug-in application provided as part of the reference implementation can be launched using the command DEMO IBM Equity. It demonstrates how easy it is to start creating compelling applications targeted for financial professionals using our proposed architecture, the Bloomberg API and WPF.

The DEMO plug-in displays historical pricing information for a given security, which is functionality found in any financial application (see Figure 6). The DEMO plug-in uses the Bloomberg DAPI and requires a valid subscription to the Bloomberg Professional service. More information about the Bloomberg API can be found at openbloomberg.com/open-api.

The DEMO Plug-In
Figure 6 The DEMO Plug-In

The XAML shown in Figure 7 defines the UI of the DEMO plug-in. The points of interest are instantiation of the Chart, LinearAxis, DateTimeAxis and LineSeries classes, as well as the setup of binding for LineSeries DependentValuePath and IndependentValuePath. We decided to use WpfToolkit for data visualization, because it works well in a partially trusted environment, provides required functionality and is licensed under the Microsoft Public License (MS-PL).

Figure 7 The DEMO Plug-in XAML

<UserControl x:Class="DapiSample.MainView"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:c=
    "clr-namespace:System.Windows.Controls.DataVisualization.Charting;
    assembly=System.Windows.Controls.DataVisualization.Toolkit"
  xmlns:v=
    "clr-namespace:System.Windows.Controls.DataVisualization;
    assembly=System.Windows.Controls.DataVisualization.Toolkit"
  Height="800" Width="1000">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <c:Chart x:Name=
        "_chart" Background="White" Grid.Row="1" Visibility="Hidden">
        <c:Chart.Axes>
          <c:LinearAxis x:Name=
            "_linearAxis" Orientation="Y" ShowGridLines="True"/>
          <c:DateTimeAxis x:Name=
            "_DateAxis" Orientation="X" ShowGridLines=
            "True" Interval="1" IntervalType="Months" />
        </c:Chart.Axes>
        <c:LineSeries x:Name=
          "_lineSeries" DependentValuePath="Value"
          IndependentValuePath="Date" ItemsSource="{Binding}"/>
        <c:Chart.LegendStyle>
          <Style TargetType="{x:Type v:Legend}">
            <Setter Property="Width" Value="0"></Setter>
            <Setter Property="Height" Value="0"></Setter>
          </Style>
        </c:Chart.LegendStyle>
      </c:Chart>
    <TextBox Grid.Row="0" x:Name=
      "_security" IsReadOnly="True" TextAlignment="Center"/>
  </Grid>
</UserControl>

In order to access the Bloomberg API, a reference to the Bloomberglp.Blpapi assembly should be added to the project references list, and the following code has to be added to the list of using statements:

using Bloomberglp.Blpapi;

The application starts by establishing a new API session and obtaining a Reference Data Service (RDS) object, used for static pricing, historical data, and intraday tick and bar requests, as shown in Figure 8.

Figure 8 Obtaining a Reference Data Service Object

private Session _session;
private Service _refDataService;
var sessionOptions = new SessionOptions
  {
    ServerHost = "localhost",
    ServerPort = 8194,
    ClientMode = SessionOptions.ClientModeType.DAPI
  };
_session = new Session(sessionOptions, ProcessEventCallBack);
if (_session.Start())
{
  // Open service
  if (_session.OpenService("//blp/refdata"))
  {
    _refDataService = _session.GetService("//blp/refdata");
  }
}

The next step is to request the historical pricing information for a given market security.

Create a Request object of type HistoricalDataRequest and construct the request by specifying the security, field (PX_LAST -last price), periodicity, and start and end dates in the format of YYYYMMDD (see Figure 9).

Figure 9 Requesting Historical Pricing Information

public void RequestReferenceData(
  string security, DateTime start, DateTime end, string periodicity)
{
  Request request = _refDataService.CreateRequest("HistoricalDataRequest");
  Element securities = request.GetElement("securities");
  securities.AppendValue(security);
  Element fields = request.GetElement("fields");
  fields.AppendValue("PX_LAST");
  request.Set("periodicityAdjustment", "ACTUAL");
  request.Set("periodicitySelection", periodicity);
  request.Set("startDate", string.Format(
    "{0}{1:D2}{2:D2}", start.Year, start.Month, start.Day));
  request.Set("endDate", string.Format(
    "{0}{1:D2}{2:D2}", end.Year, end.Month, end.Day));
  _session.SendRequest(request, null);
}

The final steps, shown in Figure 10, are to process the RDS response message asynchronously, construct a time series and visualize the data by setting the _chart.DataContext property.

Figure 10 Processing the Reference Data Service Response Message

private void ProcessEventCallBack(Event eventObject, 
    Session session)
{
  if (eventObject.Type == Event.EventType.RESPONSE)
  {
    List<DataPoint> series = new List<DataPoint>();
    foreach (Message msg in eventObject)
    {
      var element = msg.AsElement;
      var sd = element.GetElement("securityData");
      var fd = sd.GetElement("fieldData");
      for (int i = 0; i < fd.NumValues; i++)
      {
        Element val = (Element)fd.GetValue(i);
        var price = (double)val.GetElement("PX_LAST").GetValue();
        var dt = (Datetime)val.GetElement("date").GetValue();
        series.Add(new DataPoint(
          new DateTime(dt.Year, dt.Month, dt.DayOfMonth),
          price));
      }
      if (MarketDataEventHandler != null)
        MarketDataEventHandler(series);
    }
  }
}
private void OnMarketDataHandler(List<DataPoint> series)
{
  Dispatcher.BeginInvoke((Action)delegate
  {
    _chart.DataContext = series;
  });
}

Build Your Own

We presented a generic architecture for hosting untrusted .NET WPF-based plug-ins that was successfully used for implementation of the Bloomberg App Portal platform. The code download accompanying this article can help you build your own plug-in hosting solution, or it might inspire you to build an application for the Bloomberg App Portal using the Bloomberg API.


Gennady Slobodsky *is an R&D manager and architect at Bloomberg L.P. He specializes in building products utilizing Microsoft and open source technologies. A Gotham dweller, he enjoys long walks in Central Park and along Museum Mile. *

Levi Haskell is an R&D team leader and architect at Bloomberg L.P. He enjoys dissecting .NET internals and building enterprise systems.

Thanks to the following technical experts for reviewing this article: Reid Borsuk (Microsoft) and David Wrighton (Microsoft)