The Managed Extensibility Framework: Introduction

by Larry Spencer Sunday, June 12, 2011 9:12 PM

With .NET 4.0, the Managed Extensibility Framework (MEF) has graduated from CodePlex to become a first-class member of the .NET Framework.

Building software with MEF (or "composing" it, to use the MEF lingo) falls between normal linking and service-oriented architecture. Once MEF has worked its magic, you are working with in-process procedure calls, but the composition process that gets you there is very loosely coupled.

As we will see in this series of posts, MEF is an example of virtuoso software engineering. It is extraordinarily easy to use at a basic level yet, thanks to its provider-based model, it is extensible in every way imaginable. Even if you have no use for MEF, it's worth studying just for the engineering pointers.

Let's begin with the simplest possible case, which may also be the most common one: loading plug-ins from an assembly's directory. Our application will take strings from one MEF-supplied data source and broadcast them through multiple MEF-supplied publishers.

Here is what the result will look like coming through the console publisher:

Haiku: Ten Thousand Dollar Screen

The Solution

The Visual Studio solution has five projects:

  • MefIntro.Broker – Contains the main program. It’s important to realize that this program only links directly to MefIntro.Types. It does not know about the ConsolePublisher, EventLogPublisher or HaikuProvider.
  • MefIntro.ConsolePublisher – Publishes strings to the console.
  • MefIntro.EventLogPublisher – Publishes strings to the Application Event Log.
  • MefIntro.HaikuProvider – Provides strings to the broker: specifically, haikus.
  • MefIntro.Types – Contains the interfaces that providers and publishers must implement.

The ConsolePublisher, EventLogPublisher and HaikuProvider copy their assemblies to the bin\debug or bin\release directory of the Broker using a post-build event. This simulates dropping these assemblies in the deployment directory at installation time.

Post Build Event

Now let’s walk through the MEF process a project at a time.

The MefIntro.Types Project

The interfaces in this project are self-explanatory. IStringProvider is what the HaikuProvider will implement; ConsolePublisher and EventLogPublisher will implement IStringPublisher.

namespace MefIntro.Types
{
    public interface IStringProvider
    {
        /// <summary>
        /// Open the string provider and be ready to
        /// read the first string.
        /// </summary>
        void Open();

        /// <summary>
        /// Get the next string.
        /// </summary>
        /// <returns>The next string, or null if done.</returns>
        string GetNext();

        /// <summary>
        /// Close the string provider.
        /// </summary>
        void Close();
    }
}
namespace MefIntro.Types
{
    /// <summary>
    /// Publishes a string to somewhere.
    /// </summary>
    public interface IStringPublisher
    {
        /// <summary>
        /// Open the publisher.
        /// </summary>
        void Open();

        /// <summary>
        /// Write the string.
        /// </summary>
        void Write(string str); 

        /// <summary>
        /// Close the publisher.
        /// </summary>
        void Close();
    }
}

The MefIntro.Broker Project

Note: Like all the other projects that use MEF, this project references the System.ComponentModel.Composition namespace.

Program.cs

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

namespace MefIntro.Broker
{
    class Program
    {
        static void Main(string[] args)
        {
            // Set up MEF
            var myPath = Path.GetDirectoryName(
                Assembly.GetExecutingAssembly().Location);
            var catalog = new DirectoryCatalog(myPath);
            CompositionContainer container = new CompositionContainer(catalog);

            // Instatiate the object whose Imports must be satisfied.
            var broker = new MyBroker();

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

            // Process.
            broker.GetAndBroadcastStrings();
            Console.WriteLine();
            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }
    }
}

The main routine begins by creating a MEF "catalog" of all the files in the executing assembly's directory. MEF supports several types of catalogs, and you can also make your own; these other catalog types will be subjects of future posts.

The CompositionContainer contains the smarts of the operation. It knows how to gather parts from the catalog to compose the end results you request. In our case, we ask the container to "satisfy imports once." What does this mean? That will become clear in the next module.

MyBroker.cs

using System.Collections.Generic;
using System.ComponentModel.Composition;
using MefIntro.Types;

namespace MefIntro.Broker
{
    class MyBroker
    {
        [Import]
        IStringProvider _provider = null;

        [ImportMany]
        IEnumerable<IStringPublisher> _publishers = null;

        public void GetAndBroadcastStrings()
        {
            _provider.Open();
            foreach (var pub in _publishers)
                pub.Open();

            string str;
            while ((str = _provider.GetNext()) != null)
            {
                foreach (var pub in _publishers)
                    pub.Write(str);
            }

            _provider.Close();
            foreach (var pub in _publishers)
                pub.Close();
        }        
    }
}

Notice that MyBroker's two fields have [Import] and [ImportMany] attributes, respectively. These attributes are in the System.ComponentModel.Composition namespace, and tell MEF that it should instantiate and plug in objects at run time.

The [Import] attribute tells MEF that it should find exactly one class in the catalog (the DirectoryCatalog in our case) with a matching Export. If it does not find exactly one, an Exception is thrown.

[ImportMany] tells MEF to collect all the Exported IStringPublishers in the catalog and put their instantiations in an IEnumerable.

The import operation happens at that SatisfyImportsOnce call in Program.cs.

With the imports completed, Program.cs can call MyBroker's GetAndBroadcastStrings method, which is self-explanatory. You'll see that the import-fulfilled fields are used exactly as if the program had done a normal, hard link to the assemblies.

The MefIntro.HaikuProvider Project

The RandomHaiku class selects a poem at random from its static array and provides it a line at a time. Note the [Export] attribute. That's what tells MEF, "Here's something that could fulfill an Import request." If the Export attribute did not have the Type parameter, MEF would export the RandomHaiku class, not the interface. Our Import was of the interface, and we must make the Export match exactly, hence the Type.

using System;
using System.ComponentModel.Composition;
using MefIntro.Types;

namespace MefIntro.HaikuProvider
{
    /// <summary>
    /// Implements IStringProvider by providing a random Haiku.
    /// </summary>
    /// <remarks>
    /// Thanks to http://www.savageresearch.com/humor/ComputerHaiku.html
    /// </remarks>
    [Export(typeof(IStringProvider))]
    public class RandomHaiku : IStringProvider
    {
        #region Fields
        static string[,] _haikus = new string[,]
        {
            {
                "Three things are certain:",
                "Death, taxes, and lost data.",
                "Guess which has occurred."
            },
            // ...<snip>...
            {
                "Serious error.",
                "All shortcuts have disappeared.",
                "Screen. Mind. Both are blank.",
            },
        };

        static int _linesPerHaiku = 3;

        // The index of a haiku, selected at random.
        int _haikuIndex;

        // The index of the next line within the selected haiku.
        int _nextLineIndex;
        #endregion

        #region IStringProvider Implementation
        public void Open()
        {
            _haikuIndex = new Random()
                .Next(_haikus.Length / _linesPerHaiku);

            _nextLineIndex = 0;
        }

        public string GetNext()
        {
            if (_nextLineIndex < _linesPerHaiku)
                return _haikus[_haikuIndex, _nextLineIndex++];
            return null;
        }

        public void Close()
        {
            // No action necessary in this implementation.
        }
        #endregion
    }
}

The Publisher Projects

The ConsolePublisher and EventLogPublisher are conceptually trivial. The only thing to note, once again, is the Export attribute.

using System;
using System.ComponentModel.Composition;
using MefIntro.Types;

namespace MefIntro.ConsolePublisher
{
    [Export(typeof(IStringPublisher))]
    public class ConPublisher : IStringPublisher
    {
        public void Open()
        {
            // Not necessary in this implementation.
        }

        public void Write(string str)
        {
            Console.WriteLine(str);
        }

        public void Close()
        {
            // Not necessary in this implementation.
        }
    }
}

 

using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Text;
using MefIntro.Types;

namespace MefIntro.EventLogPublisher
{
    [Export(typeof(IStringPublisher))]
    public class EvLogPublisher : IStringPublisher
    {
        string _logSource = "MEF Intro";
        EventLog _log;
        StringBuilder _strings = new StringBuilder();

        public void Open()
        {
            if (!EventLog.SourceExists(_logSource))
            {
                var crData = new EventSourceCreationData(_logSource, null);
                EventLog.CreateEventSource(crData);
            }
            _log = new EventLog();
            _log.Source = _logSource;
        }

        public void Write(string str)
        {
            _strings.AppendLine(str);
        }

        public void Close()
        {
            _log.WriteEntry(
                _strings.ToString(), 
                EventLogEntryType.Information);
            _log.Dispose();
        }
    }
}

As I mentioned, the publisher and the two providers merely copy their respective output assemblies to the Broker's bin directory. The Broker picks them up in the DirectoryCatalog and the broadcasts the result to both the console and the Event Log:

Output to Console

Output to Event Log

Next time, we will look at some catalogs other than the DirectoryCatalog.

Tags: ,

All | MEF

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.