Comparing .Net IoC containers, part seven: MEF
The Managed Extensibility Framework from Microsof is part of the .Net 4.0 framework, but is not an IoC container. Well, not quite, but so close that you might take it for one. It is an Extensibility framework. It really shines when you want to know about all the classes that implement some interface (say IPlugin or IApplicationPart), and get notified when a new one shows up at runtime. So you can get told about all the plugins or components that your application has been given.
Trying to make MEF into an IoC container is the wrong way around. Many Ioc containers are coming with MEF plugins, so that they can use objects from MEF.
But nevertheless, I will try to do the same IoC things with MEF. Trying to use it as an IoC container, you run into issues. The default way of working is to attribute the types - they are no longer Plain old C# objects, they depnd on MEF's attribute types, even if they are part of the .Net 4.0 framework, in the namespaceSystem.ComponentModel.Composition
The config is scattered throughout the file. This is not a bad idea for MEF's intended usage scenario, since the types are self-describing, and adding the new assembly is then all that is needed to register it as a component or plugin. But it's not the best for these IoC tests.
Here's the MEF version of the classes that I use in these tests. Note the attributes to allow this class to be consumed, and to specify what the constructor consumes:
[Export(typeof(IJellybeanDispenser))]
public class VanillaJellybeanDispenser : IJellybeanDispenser
{
public Jellybean DispenseJellybean()
{
return Jellybean.Vanilla;
}
}
[Export(typeof(IJellybeanDispenser))]
public class StrawberryJellybeanDispenser : IJellybeanDispenser
{
public Jellybean DispenseJellybean()
{
return Jellybean.Strawberry;
}
}
[Export]
public class SweetVendingMachine
{
public IJellybeanDispenser JellybeanDispenser { get; private set; }
[ImportingConstructor]
public SweetVendingMachine([Import] IJellybeanDispenser jellybeanDispenser)
{
this.JellybeanDispenser = jellybeanDispenser;
}
}
[Export]
public class SweetShop
{
public SweetVendingMachine SweetVendingMachine { get; private set; }
[ImportingConstructor]
public SweetShop([Import] SweetVendingMachine sweetVendingMachine)
{
this.SweetVendingMachine = sweetVendingMachine;
}
public virtual Jellybean DispenseJellyBean()
{
return this.SweetVendingMachine.JellybeanDispenser.DispenseJellybean();
}
}
And here are the two tests that pass:
[Test]
public void CanMakeSweetShopWithVanillaJellybeans()
{
TypeCatalog catalog = new TypeCatalog(typeof(VanillaJellybeanDispenser), typeof(SweetVendingMachine), typeof(SweetShop));
CompositionContainer container = new CompositionContainer(catalog);
SweetShop sweetShop = container.GetExport<SweetShop>().Value;
Assert.AreEqual(Jellybean.Vanilla, sweetShop.DispenseJellyBean());
}
[Test]
public void CanMakeSweetShopWithStrawberryJellybeans()
{
TypeCatalog catalog = new TypeCatalog(typeof(StrawberryJellybeanDispenser), typeof(SweetVendingMachine), typeof(SweetShop));
CompositionContainer container = new CompositionContainer(catalog);
SweetShop sweetShop = container.GetExport<SweetShop>().Value;
Assert.AreEqual(Jellybean.Strawberry, sweetShop.DispenseJellyBean());
}
You get a singleton instance by default. Note that I can swap between the two kinds of Jellybean Dispenser by only putting one into the catalog. Of course, I could also use an AssemblyCatalogto read all types in an assembly, but then MEF would in this case not know which kind of IJellybeanDispenser to inject into the SweetVendingMachine.
Attributes, like app.config files, are fixed for the lifespan of a program's run, so like with spring, testing more than once scenario in a test run is hard.
After asking on twitter on other ways of using MEF, I got some demo code which I have lightly adapted into the custom catalog type shown below. Based on that, I got the basic type-based tests working.
If you want to do this yourself, have a look at the code in the MEF Contrib library at http://mefcontrib.codeplex.com/, which contains code to make MEF do lots of things, including this.
Note how the container is immutable once the catalog has been added into it. It's probably possible to make MEF do all the other tests, but it's not built in. MEF is not aimed directly at these scenarios.
MEF Helper code: ConventionalCatalog
public class ConventionalCatalog : ComposablePartCatalog
{
private readonly List<ComposablePartDefinition> parts = new List<ComposablePartDefinition>();
public void RegisterType<TImplementation>()
{
RegisterType<TImplementation, TImplementation>();
}
public void RegisterType<TImplementation, TContract>()
{
var part = ReflectionModelServices.CreatePartDefinition(
new Lazy<Type>(() => typeof(TImplementation)),
false,
new Lazy<IEnumerable<ImportDefinition>>(() => GetImportDefinitions(typeof(TImplementation))),
new Lazy<IEnumerable<ExportDefinition>>(() => GetExportDefinitions(typeof(TImplementation), typeof(TContract))),
new Lazy<IDictionary<string, object>>(() => new Dictionary<string, object>()),
null);
this.parts.Add(part);
}
private static IEnumerable<ImportDefinition> GetImportDefinitions(Type implementationType)
{
var constructors = implementationType.GetConstructors()[0];
var imports = new List<ImportDefinition>();
foreach (var param in constructors.GetParameters())
{
var cardinality = GetCardinality(param);
var importType = cardinality == ImportCardinality.ZeroOrMore ? GetCollectionContractType(param.ParameterType) : param.ParameterType;
var currentParam = param;
imports.Add(
ReflectionModelServices.CreateImportDefinition(
new Lazy<ParameterInfo>(() => currentParam),
AttributedModelServices.GetContractName(importType),
AttributedModelServices.GetTypeIdentity(importType),
Enumerable.Empty<KeyValuePair<string, Type>>(),
cardinality,
CreationPolicy.Any,
null));
}
return imports.ToArray();
}
private static ImportCardinality GetCardinality(ParameterInfo param)
{
if (typeof(IEnumerable).IsAssignableFrom(param.ParameterType))
{
return ImportCardinality.ZeroOrMore;
}
return ImportCardinality.ExactlyOne;
}
//This is hacky! Needs to be cleaned up as it makes many assumptions.
private static Type GetCollectionContractType(Type collectionType)
{
var itemType = collectionType.GetGenericArguments().First();
var contractType = itemType.GetGenericArguments().First();
return contractType;
}
private static IEnumerable<ExportDefinition> GetExportDefinitions(Type implementationType, Type contractType)
{
var lazyMember = new LazyMemberInfo(implementationType);
var contracName = AttributedModelServices.GetContractName(contractType);
var metadata = new Lazy<IDictionary<string, object>>(() =>
{
var md = new Dictionary<string, object>();
md.Add(CompositionConstants.ExportTypeIdentityMetadataName, AttributedModelServices.GetTypeIdentity(contractType));
return md;
});
return new[] { ReflectionModelServices.CreateExportDefinition(lazyMember, contracName, metadata, null) };
}
public override IQueryable<ComposablePartDefinition> Parts
{
get
{
return this.parts.AsQueryable();
}
}
public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)
{
return base.GetExports(definition);
}
}
MEF Tests using the ConventionalCatalog
[TestFixture]
public class MEFTests2
{
[Test]
public void CanMakeSweetShopWithVanillaJellybeans()
{
var catalog = new ConventionalCatalog();
catalog.RegisterType<VanillaJellybeanDispenser, IJellybeanDispenser>();
catalog.RegisterType<SweetVendingMachine>();
catalog.RegisterType<SweetShop>();
var container = new CompositionContainer(catalog);
SweetShop sweetShop = container.GetExport<SweetShop>().Value;
Assert.AreEqual(Jellybean.Vanilla, sweetShop.DispenseJellyBean());
}
[Test]
public void CanMakeSweetShopWithStrawberryJellybeans()
{
var catalog = new ConventionalCatalog();
catalog.RegisterType<StrawberryJellybeanDispenser, IJellybeanDispenser>();
catalog.RegisterType<SweetVendingMachine>();
catalog.RegisterType<SweetShop>();
var container = new CompositionContainer(catalog);
SweetShop sweetShop = container.GetExport<SweetShop>().Value;
Assert.AreEqual(Jellybean.Strawberry, sweetShop.DispenseJellyBean());
}
[Test]
public void CanMakeAniseedRootObject()
{
var catalog = new ConventionalCatalog();
catalog.RegisterType<AniseedSweetShop, SweetShop>();
var container = new CompositionContainer(catalog);
SweetShop sweetShop = container.GetExport<SweetShop>().Value;
Assert.AreEqual(Jellybean.Aniseed, sweetShop.DispenseJellyBean());
}
[Test]
public void CanUseAnyJellybeanDispenser()
{
SweetShop sweetShop = new SweetShop(new SweetVendingMachine(new AnyJellybeanDispenser(Jellybean.Strawberry)));
Assert.AreEqual(Jellybean.Lemon, sweetShop.DispenseJellyBean());
}
}
