Thursday, October 7, 2010

Fun With MEF

I've been playing with MEF. I wanted to use it in the simplest way I could.

For those who don't know, MEF is a component that you can use to manage objects in your application. It's in the universe of "Dependency Injection". It has some features that make it really good for "Plug-In" architectures, but I'll be using it for object lifetime management. There is much less "noise" in using MEF, than the other (SimpleServiceLocator) DI that I have used. In many ways DI is like the Factory pattern on steroids. In any case, you decorate the class that you want to provide as a component with the Export attribute, and you mark the properties of your dependent classes with the Import attribute. MEF scans the assemblies in your directory for all the Exports, and scans all the Imports, and does a quick fix-up, matching Exports to Imports. If I need a IFileSystem object in my logger, I "automagically" get one. I don't need to new one up, or MyFileSystemFactory.Create(), or first register a constructor or factory with a static ServiceLocator. See Glenn's article for an in-depth tour.


I ran in to the problem that the Imports in a client assembly were not being satisfied, while the imports in the assembly where my MEF host class is defined were satisfied. Here is the ExtensibilityHost class that is in the Heat.dll host assembly:



//Heat.cs
namespace Heat
{
public static class Constants {
public const string NullOrEmptyString = "null or empty string";
public const string NullObject = "null object";
public const string EmptyString = "empty string";
public const string RangeError = "range error";
public const string SequenceError = "sequence error";
public const string ArrayLengthError = "array length error";
}
}





//HeatExtensibility.cs

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

using Heat;
namespace Heat.Extensibility
{
public interface IExtensibilityHost : IDisposable
{
void Compose();
void SatisfyImports(object o);
}
public static class ExtensibilityHostFactory
{
private static IExtensibilityHost host = null;
public static IExtensibilityHost Get()
{
if (null == host) host = new ExtensibiltyHost();
return host;
}
private class ExtensibiltyHost : IExtensibilityHost
{
private enum Sequence
{
Initial = 0,
Compose = 1,
SatisfyImports = 2
}
private Sequence sequence = Sequence.Initial;
private CompositionContainer compositionContainer;
private AssemblyCatalog assemblyCatalog;
private DirectoryCatalog directoryCatalog;
public void Compose()
{
Contract.Requires(this.sequence == Sequence.Initial, Constants.SequenceError);
this.assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
this.directoryCatalog = new DirectoryCatalog((new FileInfo(Assembly.GetExecutingAssembly().Location)).DirectoryName);
this.compositionContainer = new CompositionContainer(this.directoryCatalog);
this.compositionContainer.ComposeParts(this);
this.sequence = Sequence.Compose;
}
public void SatisfyImports(object o)
{
Contract.Requires(null != o, Constants.NullObject);
Contract.Requires(this.sequence != Sequence.Initial, Constants.SequenceError);
this.compositionContainer.SatisfyImportsOnce(o);
this.sequence = Sequence.SatisfyImports;
}
virtual public string AssemblyInfo()
{
return Assembly.GetExecutingAssembly().ToString();
}
virtual public void Dispose()
{
Contract.Requires(this.sequence != Sequence.Initial, Constants.SequenceError);
this.assemblyCatalog.Dispose();
this.directoryCatalog.Dispose();
this.compositionContainer.Dispose();
this.sequence = Sequence.Initial;
}
}
}
}




Here is the exported object. When a client assembly needs an IFileSystem object, MEF will find it (Heat.dll), instatiate only one (singleton), and deliver it to the instance of the class in need of it.




//HeatFileSystem.cs
using System.IO;
using Heat.Contracts;// IFileSystem
namespace Heat.Utility
{
[Export(typeof(IFileSystem))]
public class FileSystem : IFileSystem
{…}
}



Fortunatly, I work near The Guy who introduced me to MEF, and he steared me in the right direction. Thanks Glenn! It turns out IExtensibilityHost.Compose() will resolve the the dependencies in the Host assembly, it does not do this for the client assembly. In order satisfy those, I'll need to call another function, passing in an instance of the object that contains the imports. IExtesabilityHost.SatisfyImports(this) ends up calling the SatisfyImportsOnce function of the MEF composition container.

You'll know that this happens, because the TEST FileSystemTest.FileSystem() passes.

The UnitTestHeatFileSystem.cs file contributes to the client assembly UnitTest.dll.


//UnitTestHeatFileSystem.cs
using Heat.Extensibility;
using Heat.Contracts;//IFileSystem
using Heat.Tests.UnitTest;//ITest
using NUnit.Framework;
namespace Heat.Test.FileSystem
{
[TestFixture]
public class FileSystemTest : ITest
{
private IExtensibilityHost extensibilityHost = null;
[Import(typeof(IFileSystem))]
private IFileSystem filesystem;
[SetUp]
public void Setup()
{
this.extensibilityHost = ExtensibilityHostFactory.Get();
this.extensibilityHost.Compose();
this.extensibilityHost.SatisfyImports(this);
}
[TearDown]
public void TearDown() { this.extensibilityHost.Dispose(); }
[Test]
public void Sanity() { Assert.IsTrue(true); }
[Test]
public void FileSystem()
{
Assert.IsTrue(null != this.filesystem);
}
:
}
}

No comments:

Post a Comment