Freigeben über


A data-driven MEF bootstrapper

Overview

MEF (Managed Extensibility Framework), introduced in .NET 4.0 (under System.ComponentModel.Composition), provides a powerful framework for creating extensible applications. Traditionally, extensible applications have a large and often fixed core with specific extension points to allow the application to be extended in very particular ways. While limited extensibility is sometimes preferred, it is often desirable to maximize extensibility. When used to its fullest, MEF allows an application to be almost entirely composed of loosely coupled components. With such a design, core components and extensions become one and the same. In the approach I discuss here, the only remaining fixed aspect of the application is the bootstrapper. The bootstrapper is responsible for the following:

  1. Preparing the part catalog (ComposablePartCatalog) and initializing the container (CompositionContainer)
  2. Discovering and executing the pseudo entry points

Preparing the part catalogs

Typical examples of MEF-based applications use an AggregateCatalog that aggregates TypeCatalogs and/or AssemblyCatalogs (which contain the fixed components of the core application) and a DirectoryCatalog (which contains the application’s extensions, discovered from assemblies on disk). This strategy is probably sufficient if your goal is to implement a single partially extensible application and not share components between multiple applications. This strategy is probably not sufficient if your goal is to implement multiple fully extensible applications composed of generic shared components and application-specific non-shared components. To satisfy this goal, a more controlled mechanism than DirectoryCatalog is needed for specifying the parts that compose a particular application. TypeCatalog and AssemblyCatalog provide more granular control, but we need a data-driven way of defining these catalogs to satisfy the goal of extensibility.

The approach I take here is to simply encode various catalogs as xaml documents stored on disk next to the application executable(s). This is possible due to improvements in the xaml language and API in .NET 4.0. For example, an executable called MyApplication.exe might have xaml catalog files in the same directory called MyApplication.Core.catalog, MyApplication.Extension1.catalog, MyApplication.Extension2.catalog, etc. MyApplication.Core.catalog could look something like this:

<AssemblyCatalog
    xmlns="clr-namespace:System.ComponentModel.Composition.Hosting;assembly=System.ComponentModel.Composition"
xmlns:x=https://schemas.microsoft.com/winfx/2006/xaml
x:Arguments="MyApplication.exe" />

This catalog would expose all parts exported in the primary executable. Alternatively, MyApplication.Core.catalog could be much more selective and look more like this:

<TypeCatalog
    xmlns="clr-namespace:System.ComponentModel.Composition.Hosting;assembly=System.ComponentModel.Composition"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<x:Arguments>
<x:Array Type="sys:Type">
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartA, PartLibrary1" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartB, PartLibrary1" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartC, PartLibrary2" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartD, PartLibrary2" />
</x:Array>
</x:Arguments>
</TypeCatalog>

Alternatively, the two might be combined with an AggregateCatalog as follows:

<AggregateCatalog
xmlns="clr-namespace:System.ComponentModel.Composition.Hosting;assembly=System.ComponentModel.Composition"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:mefp="clr-namespace:System.ComponentModel.Composition.Primitives;assembly=System.ComponentModel.Composition"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<x:Arguments>
<x:Array Type="mefp:ComposablePartCatalog">

<AssemblyCatalog x:Arguments="MyApplication.exe" />

<TypeCatalog>
<x:Arguments>
<x:Array Type="sys:Type">
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartA, PartLibrary1" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartB, PartLibrary1" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartC, PartLibrary2" />
<sys:Type x:FactoryMethod="sys:Type.GetType" x:Arguments="PartD, PartLibrary2" />
</x:Array>
</x:Arguments>
</TypeCatalog>

</x:Array>
</x:Arguments>
</AggregateCatalog>

These catalogs could be as simple or as complex as necessary, and catalogs could be added or removed at any time without recompiling the application. Additionally, using the XamlExtension MarkupExtension I blogged about previously, catalogs could easily be shared as follows:

<AggregateCatalog
xmlns="clr-namespace:System.ComponentModel.Composition.Hosting;assembly=System.ComponentModel.Composition"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:mefp="clr-namespace:System.ComponentModel.Composition.Primitives;assembly=System.ComponentModel.Composition"
xmlns:xe="clr-namespace:XamlExtensions;assembly=XamlExtensions"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<x:Arguments>
<x:Array Type="mefp:ComposablePartCatalog">

<AssemblyCatalog x:Arguments="MyApplication.exe" />

<xe:Xaml Source="CommonExtension1.catalog" />

</x:Array>
</x:Arguments>
</AggregateCatalog>

The code to load the xaml encoded catalogs is pretty straight forward:

public static class XamlCatalogServices
{
public static ComposablePartCatalog LoadCatalog(string filePath)
{
var catalog = XamlServices.Load(filePath) as ComposablePartCatalog;
if (catalog == null)
{
throw new InvalidOperationException(string.Format("{0} is not a ComposablePartCatalog", filePath));
}
return catalog;
}

public static IEnumerable<ComposablePartCatalog> LoadCatalogs(string directory = null, string searchPattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly)
{
if (string.IsNullOrEmpty(directory))
{
directory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
}

if (string.IsNullOrEmpty(searchPattern))
{
searchPattern = string.Format("{0}*.catalog", Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location));
}

return Directory.EnumerateFiles(directory, searchPattern, searchOption).Select(filePath => XamlCatalogServices.LoadCatalog(filePath));
}
}

Discovering and executing the pseudo entry points

Since my goal includes implementing a small, generic bootstrapper that can be used as the basis for any MEF-based application, the concept of an entry point must be abstracted. The application executable, which hosts the bootstrapper, will of course have the real entry point, but the bootstrapper will discover and execute pseudo entry points on a per-application basis. The approach I take here is to simply have the bootstrapper query the container (after the catalogs have been initialized) for an IEntryPoint and invoke it. I defined IEntryPoint as follows:

public interface IEntryPoint
{
void BeginExecution();
}

An application could implement its own custom IEntryPoint, but I’ve found a few generic implementations to be sufficient in most cases. The first I called PartActivatingEntryPoint, which gets startup components running. This entry point “activates” parts with the “AutoActivate” contract. Examples of such parts might be the main window of an application, background services that are not consumed by other components but provide important functionality (such an auto-save component), etc. I defined PartActivationEntryPoint as follows:

[Export(typeof(IEntryPoint))]
public class PartActivatingEntryPoint : IEntryPoint
{
[ImportMany("AutoActivate")]
private IEnumerable<Lazy<object>> autoActivateParts;

public virtual void BeginExecution()
{
this.autoActivateParts.Select(part => part.Value).ToArray();
}
}

A subclass of PartActivatingEntryPoint that I’ve found useful is one I called WpfApplicationEntryPoint. It instantiates a System.Windows.Application class and then activates startup parts within the context of the Dispatcher. I defined WpfApplicationEntryPoint as follows:

[Export(typeof(IEntryPoint))]
public class WpfApplicationEntryPoint : PartActivatingEntryPoint
{
public override void BeginExecution()
{
var application = new Application();
application.Startup += delegate { base.BeginExecution(); };
application.Run();
}
}

Putting the pieces together

I’ve described my approach to preparing the part catalog and discovering and executing the pseudo entry points, and provided sample code showing basic implementations. The only remaining part is to pull these pieces together into the actual bootstrapper. This is easily done as follows:

public static class MEFBootstrapper
{
public static void Run()
{
// Discover the catalogs and create the container
var container = new CompositionContainer(new AggregateCatalog(XamlCatalogServices.LoadCatalogs()));

// Query the container for the entry point
var entryPoint = container.GetExportedValueOrDefault<IEntryPoint>();
if (entryPoint == null)
{
throw new InvalidOperationException("No entry point found. Add an IEntryPoint to the container.");
}

// Invoke the entry point
entryPoint.BeginExecution();
}
}

MEFBootStrapper.Run can be invoked from the real application entry point. For example:

internal static class Program
{
[STAThread]
private static void Main()
{
MEFBootstrapper.Run();
}
}

In fact, the same executable containing the Main method shown above could be used as the starting point for any MEF-based application that uses the bootstrapper that I’ve defined here (ok, a recompile of this executable would be necessary for a console application).

I’ve found this simple approach to composing extensible applications from loosely coupled reusable parts to be very successful. I hope others find it useful as well. I’ve made some sample code available as a simplified but complete demonstration of this approach. In the sample code, I deliberately made some parts “auto activated” while others are not since this is what I would expect in a real application. Feedback is welcome.