Managed Extensibility Framework (MEF): Aggregate Catalog

by Larry Spencer Friday, June 17, 2011 8:20 AM

In the first post of this series, we used MEF to load types dynamically from the executing assembly's directory.  In MEF parlance, we used a DirectoryCatalog.  There are other types of catalogs, too.  In this post, we will look at two of them:

  • AssemblyCatalog: Loads from an assembly.
  • AggregateCatalog: Combines several catalogs into one.

Our use case will be this: We want to create a class that implements a particular interface in a default way or, if another implementation is dropped into a directory, we want to use that one.

We will accomplish this by creating an AggregateCatalog from two sub-catalogs: an AssemblyCatalog on the current assembly, which contains the default implementation, and a DirectoryCatalog in an Overrides directory.

Here is our solution:

MefAggregate.sln

The Types Library: MefAggregate.Types

As in the previous post, the Types assembly contains the contract that our MEF-injected type will implement.  It's simpler this time, featuring only a Write method.

namespace MefAggregate.Types
{
    /// <summary>
    /// Publishes a string to somewhere.
    /// </summary>
    public interface IStringWriter
    {
        /// <summary>
        /// Write the string.
        /// </summary>
        void Write(string str);
    }
}

The Main Project: MefAggregate.Main

The main project contains a default implementation of IStringWriter. Note once again the [Export] attribute, which was explained in the introductory post.

using System;
using System.ComponentModel.Composition;
using MefAggregate.Types;

namespace MefAggregate.Main
{
    [Export(typeof(IStringWriter))]
    class DefaultWriter : IStringWriter
    {
        public void Write(string str)
        {
            Console.WriteLine("DefaultWriter: {0}", str);
        }
    }
}

 

The Program.cs file demonstrates how to combine an AssemblyCatalog and a DirectoryCatalog into one AggregateCatalog.  It is the AggregateCatalog that is fed into the CompositionContainer.  After we tell the container to satisfy the imports in the Writer object, we are ready to call writer.Write("Hello!").

using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Reflection;

namespace MefAggregate.Main
{
    class Program
    {
        static void Main(string[] args)
        {
            // Set up MEF
            var myPath = Path.Combine(
                Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
                "Overrides");
            var directoryCatalog = new DirectoryCatalog(myPath);

            var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

            var aggregateCatalog = new AggregateCatalog(assemblyCatalog, directoryCatalog);

            using (var container = new CompositionContainer(aggregateCatalog))
            {
                // Instatiate the object whose Imports must be satisfied.
                var writer = new Writer();

                // Do the Imports.
                container.SatisfyImportsOnce(writer);

                // Process.
                writer.Write("Hello!");
            }
            Console.WriteLine();
            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }
    }
}

 

The Writer class contains several points of interest.

First, note that it also implements IStringWriter, but it does not have an [Export] attribute.  We plan to use it directly, not import it with MEF. The implementation of IStringWriter merely delegates to a class that will be loaded with MEF. It selects an override implementation if there is one, or the default implementation if there isn't.

It would be nice if we could make MEF load the implementations in a certain order and we'd just pick the first one, but we can't.  MEF makes no promises about the order of the objects it instantiates.  So, we write the code in the SelectedWriter property to do it the hard way. (It is also possible to attach metadata to each import, and order based on the metadata, but we'll save metadata for another post.)

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using MefAggregate.Types;

namespace MefAggregate.Main
{
    class Writer : IStringWriter
    {
        [ImportMany]
        IEnumerable<IStringWriter> _writers = null;

        IStringWriter SelectedWriter
        {
            get
            {
                switch (_writers.Count())
                {
                    case 1:
                        return _writers.First();
                    case 2:
                        return _writers.Single(p => p.GetType() != typeof(DefaultWriter));
                    default:
                        throw new InvalidOperationException(
                            String.Format("Unexpected number of IStringWriters: {0}.",_writers.Count()));
                }
            }
        }

        public void Write(string str)
        {
            SelectedWriter.Write(str);
        }
    }
}

 

If we run the program now, we get output from the default implementation of IStringWriter:

Output Without Override

The Override Project: MefAggregate.Override

For the next part of our experiment, we want to override the default writer with one in another project, namely MefAggregate.Override.  The module there writes a slightly different message:

using System;
using System.ComponentModel.Composition;
using MefAggregate.Types;

namespace MefAggregate.Override
{
    [Export(typeof(IStringWriter))]
    public class OverrideWriter : IStringWriter
    {
        public void Write(string str)
        {
            Console.WriteLine("OverrideWriter: {0}", str);
        }
    }
}

 

If we copy the output of this project to the Overrides directory under the main project's bin\debug or bin\release directory, we can see that the override version is being used:

Output With Override

Summary

We have seen how to use an AggregateCatalog to combine more than one source of Export-decorated classes for MEF.

We have also seen a case where this can be useful, namely to allow the behavior of a class to be transparently overridden by another class.

The pattern was to add a level of indirection. The top-level class (Writer.cs) implements an interface, but it does so by delegating to a MEF-injected class that also implements the interface.  It lets MEF inject all available implementations, and then chooses the override if it is present and the default if it is not.

Tags: ,

All | MEF

Pingbacks and trackbacks (1)+

Add comment