The blog of dlaa.me

MEF addict [Combining .NET 4's type embedding and MEF to enable a smooth upgrade story for applications and their extensions]

One of the neat new features in version 4 of the .NET Framework is something called "type equivalence" or "type embedding". The basic idea is to embed at compile time all the type information about a particular reference assembly into a dependent assembly. Once this is done, the resulting assembly no longer maintains a reference to the other assembly, so it does not need to be present at run time. You can read more about type embedding in the MSDN article Type Equivalence and Embedded Interop Types.

Although type equivalence was originally meant for use with COM to make it easier to work against multiple versions of a native assembly, it can be used successfully without involving COM at all! The MSDN article Walkthrough: Embedding Types from Managed Assemblies (C# and Visual Basic) explains more about the requirements for this along with explicit steps to use type embedding with an assembly.

 

Here's a simple interface that is enabled for type embedding:

using System.Runtime.InteropServices;

[assembly:ImportedFromTypeLib("")]

namespace MyNamespace
{
    [ComImport, Guid("1F9BD720-DFB3-4698-A3DC-05E40EDC69F1")]
    public interface MyInterface
    {
        string Name { get; }
        string GetVersion();
    }
}

 

Another thing that's new with .NET 4 (though it had previously been available on CodePlex) is the Managed Extensibility Framework (MEF). MEF makes it easy to implement a "plug-in" architecture for applications where assemblies are loosely coupled and can be added or removed without explicit configuration. While there have been a variety of not-so-successful attempts to create a viable extensibility framework before this, there's general agreement that MEF is a good solution and it's already being used by prominent applications like Visual Studio.

 

Here's a simple MEF-enabled extension that implements - and exports - the interface above:

[Export(typeof(MyInterface))]
public class MyExtension : MyInterface
{
    public string Name
    {
        get { return "MyExtension"; }
    }

    ...
}

And here's a simple MEF-enabled application that uses that extension by importing its interface:

class MyApplication
{
    [ImportMany(typeof(MyInterface))]
    private IEnumerable<MyInterface> Extensions { get; set; }

    public MyApplication()
    {
        var catalog = new DirectoryCatalog(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location));
        var container = new CompositionContainer(catalog);
        container.SatisfyImportsOnce(this);
    }

    private void Run()
    {
        Console.WriteLine("Application: Version={0}", Assembly.GetEntryAssembly().GetName().Version.ToString());
        foreach (var extension in Extensions)
        {
            Console.WriteLine("Extension: Name={0} Version={1}", extension.Name, extension.GetVersion());
        }
    }

    ...
}

 

The resulting behavior is just what you'd expect:

P:\MefAndTypeEmbedding>Demo

Building...
Staging V1...
Running V1 scenario...

Application: Version=1.0.0.0
Extension: Name=MyExtension Version=1.0.0.0

...

 

However, it's important to note that MEF does not isolate an application from versioning issues! Ideally, extensions written for version 1 of an application will automatically load and run under version 2 of that application without needing to be recompiled - but you don't get that for free. There is an easy way to do this, though: avoid making any changes to the contract assembly after v1 is released. :)

Aside: The contract assembly is the place where the public interfaces of an application live. Because interfaces are generally the only thing in a contract assembly, both the application and its extensions can reference it and it can be published as part of an SDK without needing to include implementation details, too.

But because the whole point of version 2 is to improve upon version 1, it's quite likely the contract assembly will undergo some changes along the way. This is where problems come up: assuming the contract assembly was strongly-named and its assembly version updated (as it should be if its contents have changed!), v1 extensions will not load for the v2 application because they won't be able to find the same version of the contract assembly they were compiled against...

Aside: If the contract assembly was not strongly-named, then v1 extensions might be able to load the v2 version - but it won't be what they're expecting and that can lead to problems.

 

Here's an updated version of the original interface with a new Author property for version 2:

using System.Runtime.InteropServices;

[assembly:ImportedFromTypeLib("")]

namespace MyNamespace
{
    [ComImport, Guid("1F9BD720-DFB3-4698-A3DC-05E40EDC69F1")]
    public interface MyInterface
    {
        string Name { get; }
        string GetVersion();
        string Author { get; }
    }
}

 

One way to solve the versioning problem is to ship the v1 contract assembly and the v2 contract assembly along with the v2 application. (Of course, this can be tricky if both assemblies have the same file name, so you'll probably also want to name them uniquely.) Shipping multiple versions of a contract assembly works well enough (it's pretty typical for COM components), but it can also cause some confusion for Visual Studio when it sees multiple same-named interfaces referenced by the v2 application - not to mention the developer burden of managing multiple distinct versions of the "same" interface...

Fortunately, there's another way that doesn't require the v2 application to include the v1 contract assembly at all: type embedding. If the contract assembly is enabled for type embedding and v1 extension authors enable that when compiling, all the relevant bits of the contract assembly will be included with the v1 extension and there will be no need for the v1 contract assembly to be present. What that means is "reasonable" interface changes during development of the v2 application will automatically be handled by .NET and v1 extensions will work properly without any need to recompile/upgrade/etc.!

Aside: By "reasonable" interface changes, I mean removing properties or methods (and therefore not calling the v1 implementations) or adding them (which will throw MissingMethodException for v1 extensions that don't support the new property/method). Changes to existing properties and methods are trickier and probably best avoided as a general rule.

 

The v2 version of the sample application uses the Author property when it's present (for v2 extensions), but gracefully handles the case where it's not (as for v1 extensions):

private void Run()
{
    Console.WriteLine("Application: Version={0}", Assembly.GetEntryAssembly().GetName().Version.ToString());
    foreach (var extension in Extensions)
    {
        string author;
        try
        {
            author = extension.Author;
        }
        catch (MissingMethodException)
        {
            author = "[Undefined]";
        }
        Console.WriteLine("Extension: Name={0} Version={1} Author={2}", extension.Name, extension.GetVersion(), author);
    }
}

 

Here's the v2 version in action:

...

Staging V2...
Running V2 scenario...

Application: Version=2.0.0.0
Extension: Name=MyExtension Version=1.0.0.0 Author=[Undefined]
Extension: Name=MyExtension Version=2.0.0.0 Author=Me

 

[Click here to download the complete source code for the sample application/contract assembly/extensions and demo script used here.]

 

Type embedding and MEF are both fairly simple concepts that add a layer of flexibility to enable some pretty powerful scenarios. As is sometimes the case, the whole is greater than the sum of its parts and combining these two technologies provides an elegant solution to the tricky problem of upgrading an application without breaking existing plug-ins.

If you aren't already familiar with MEF or type embedding, maybe now is a good time to learn! :)

 

PS - My thanks go out to Kevin Ransom on the CLR team for providing feedback on a draft of this post. (Of course, any errors are entirely my own!)