27 April 2010

A very basic MEF sample using ImportMany

If you have been programming since the mid-80’s and have been a IT professional for over 17 years it does not happen often anymore that you get mesmerized by a beautiful piece of architecture, but recently it happened twice to me in a very short time, and one of the causes was the Managed Extensibility Framework or MEF. Almost everyone has built some or other extensible architecture at one point but never before I have seen something as simple, elegant and universally applicable as this.

This article describes the setup of a very basic MEF driven application, using VS2010 Pro and .NET 4. It’s kind of abstract: a hosting component that accepts two strings, and calls one or more MEF components to actually do the manipulation. Every component has its own library, which may seem a bit overkill, but I wanted to check out the extensibility to the max. So I started out with an empty solution and then used the following track

1. Create a class library "Contracts"
This will contain the interface IMyComponent by which the components communicate:
namespace Contracts
{
  public interface IMyComponent
  {
    string Description { get; }
    string ManipulateString(string a, string b);
  }
}

as you can see, one hell of a complex component we are making here ;-)

2. Create a class library "ImportingLib"
This will contain the host, i.e. the class that actually hosts and calls the components. Add a reference to the "Contracts" projects as well as to "System.ComponentModel.Composition". Then Add a class "Importer", with the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using Contracts;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;
using System.IO;

namespace ImportingLib
{
  public class Importer
  {
    [ImportMany(typeof(IMyComponent))]
    private IEnumerable<IMyComponent> operations;

    public void DoImport()
    {
      //An aggregate catalog that combines multiple catalogs
      var catalog = new AggregateCatalog();
      //Adds all the parts found in all assemblies in 
      //the same directory as the executing program
      catalog.Catalogs.Add(
       new DirectoryCatalog(
        Path.GetDirectoryName(
         Assembly.GetExecutingAssembly().Location)));

      //Create the CompositionContainer with the parts in the catalog
      CompositionContainer container = new CompositionContainer(catalog);

      //Fill the imports of this object
      container.ComposeParts(this);
    }

    public int AvailableNumberOfOperations
    {
      get
      {
        return (operations != null ? operations.Count() : 0);
      }
    }

    public List<string> CallAllComponents( string a, string b)
    {
      var result = new List<string>();
      foreach( var op in operations )
      {
        Console.WriteLine(op.Description);
        result.Add( op.ManipulateString(a,b ));
      }
      return result;
    }
  }
}

This deserves some explanation. This host imports 0 or more (ImportMany) components implementing IMyCompoment - and wants them delivered into the private property operations, thank you. The method "DoImport" can be called to initialize this object - Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location) gives the directory in which the executing assembly (i.e. the program or the test) project resides, and by creating a DirectoryCatalog on that directory and then adding that to the main AggregateCatalog you make MEF automatically start searching all the assemblies in the directory where the calling program resides. Property AvailableNumberOfOperations of the Importer returns the number of found operations, and CallAllComponents calls all the IMyComponent exporting components and returns the result in one go.

To prove this actually works, we continue:

3. Create a class library "ExportingLib1"
Add a reference to "Contracts" and "System.ComponentModel.Composition", then add a class "TestComponent1" with the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Contracts;
using System.ComponentModel.Composition;

namespace ExportingLib1
{
  [Export(typeof(IMyComponent))]
  public class TestComponent1 : IMyComponent
  {
    #region IMyComponent Members
    public string Description
    {
      get { return "Concatenates a and b"; }
    }

    public string ManipulateString(string a, string b)
    {
      return string.Concat(a, b);
    }
    #endregion
  }
}

as you can see, this utterly exiting class exports IMyComponent, delivers a description of itself and concatenates the two strings to 1

4. Create a class library "ExportingLib2"
Add a reference to "Contracts" and "System.ComponentModel.Composition", then add a class "TestComponent2" with the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Contracts;
using System.ComponentModel.Composition;

namespace ExportingLib2
{
  [Export(typeof(IMyComponent))]
  public class TestComponent2 : IMyComponent
  {
    #region IMyComponent Members
    public string Description
    {
      get { return "Removes b from a"; }
    }

    public string ManipulateString(string a, string b)
    {      
      return a.Replace(b, string.Empty);
    }
    #endregion
  }
}
Again, a very complex class ;-), this time it removes all occurrences from b in a.

5. Create a test project "ImportingLib.Test"

Add references to ImportingLib, ExportingLib1 and ImportingLib2 and add the following test methods:

[Test]
public void TestCountComponents()
{
  var t = new Importer();
  t.DoImport();
  Assert.AreEqual(2, t.AvailableNumberOfOperations);
}

[Test]
public void TestOperations()
{
  var t = new Importer();
  t.DoImport();
  var result = t.CallAllComponents("all are equal ", "all");
  Assert.IsTrue( result.Contains( "all are equal all"));
  Assert.IsTrue( result.Contains( " are equal "));
}
If you followed my ‘recipe’ correctly, both tests will succeed. In addition, since you have added a Console.WriteLine in the CallAllComponents method, on the standard console output you will see "Concatenates a and b" and "Removes b from a", indicating the components that have been called. As you see, nowhere in code the components are actually instantiated - this is all done by MEF. The fun thing is, just by removing the reference to either ExportingLib1 or ExportingLib2 you can make the test fail. Nowhere are any explicit links between host and components but in the actual calling program itself, and those links are only made by the very presence of an assembly in the directory of the current executing program, or in this case, the test project.

So, you can dump more assemblies exporting IMyComponent - and the host will automatically pick them up. #Thatwaseasy

Parts of this sample are based on this description

Update 06-07-2012: I've added a full demo solution to this post to show how things are put together

19 comments:

Anders said...

Hi Joost,

Thanks a lot for the beautiful example of using MEF - really the best example I have found around.

You saved my day :-)

Best regards Anders

HARSHA said...

Hi,
I am facing problem in the last part of the code.
Creating ImportingLib.Test.Which class name should be there in this project.Should the class inherit from other class.I have added all the required references.It gives me error on [TestMethod].
The Type or namespace Testmethod could not be found. You are missing a directive or assembly.

Kindly help me asap

Loc#alJoost said...

@HARSHA I use nUnit for unit tests. Please donwnload that, reference to nunit.framework.dll and you problem should be solved.

The IT Kid said...

Came across this while trying to understand MEF - really nice article and explanation! Thanks :)

Jinson Geo said...

To the point!!!

joelrondeau said...

What do you do about the CS0649 warning on operations? ('operations' is never assigned to, and will always have its default value null)?

Thomas6767 said...

Really one of the best examples for beginners.
But there is a problem with the last part. The test project should NOT reference the exportingLib, that's the whole point of MEF. You have to use the post-build event in the exportingLib to get it into the assebly: copy "$(TargetPath)" "$(SolutionDir)\bin\$(ConfigurationName)"

Joost van Schaik said...

@joelrondeau I ignore them or put pragmas around them ;-)

Joost van Schaik said...

@Thomas6767 You are right of course. I just wanted to keep things simple for the beginner.

David said...

Hi,
I'm already googling for a day without finding a solution. In my case, my contract is generic. So my interface has a Type parameter, that limited to class.

So I tried:
1. ImportMany with no type parameter
2. ImportMany with object as type parameter
3. ImportMany with the same type I exported my concret implementation.

Only the last one works. As far as I don't know what "Extension" might be implemented in future, I don't be able to specify a type for importing.

Is there something I missed?

Thanks in anticipation

David

Joost van Schaik said...

@David: apparently generic contracts are not supported out of the box. Never tried that, too. Does this link help? http://www.codeproject.com/Articles/323919/MEF-Generics

Mahesh Malpani said...

Thanks a ton!!!
really good example for concept.

Good starting point to explore MEF

Şafak Gür said...

Hi there, this is a great example though I couldn't manage to change the directory that MEF checks. I tried this:

var appPath = Assembly.GetExecutingAssembly().Location;
var subDir = Path.Combine(appPath, "plugins");
catalog.Catalogs.Add(new DirectoryCatalog(subdir));

But it still checks the application's directory and no sub-directories, especially not the plugins folder. How can I make it to look there? :)

Joost van Schaik said...

@Şafak I am afraid I don't know. Have you checked the actual paths both my and your sample code are trying to query and compare them?

Şafak Gür said...

Yes, I actually used your sample directly. The only thing I tried to change was the folder. I even tried:
catalog.Catalogs.Add(new DirectoryCatalog("C:"));
MEF checks the application path instead of C:\ for DLLs.
The weird part is that it checks the directory (it throws an exception if there is no directory at the specified path) but as long as a directory that exists is specified, it doesn't use it.

Şafak Gür said...

Oops, sorry, I got it working. I just removed the "Path.GetDirectoryName" part, guess it was getting me the parent or so, anyway, thanks again for the great example.

Joost van Schaik said...

@Şafak I love to see a problem solve itself ;-) Thanks for coming back and sharing!

Chip said...

Using ReSharper with shadow DLL's feature turned on breaks the unit tests. It is due to the Path.GetExecutingAssembly(). If you change the Importer code to use the AppDomain.BaseDirectory construct, then this is fool-proof... :-)

Thanx for you post and example code. It was golden for me!

Keng Tung Lee said...

Thanks for the example. I use it as starting point for plugin.