Cooking up some MEF
The Managed Extensibility Framework is Microsoft’s latest proposition to aid in creating an extensible application. If you follow any number of patterns today (most notably unit testing) you will eventually run across the need for a dependency injection framework, with so many on the market and having personally used several myself, I wanted to test MEF to see if it can get the job done and why I might ever want to use it.
Also for .NET Framework 4 and Silverlight 4 it comes in the box which does make for some convenience and provides a hall pass if you are unlucky enough to be in one of those shops that fends off anything not coming out of Redmond.
Right of the get go you might notice MEF is labeled as an extensibility framework, not a DI framework. When developing MEF Microsoft had a use case in mind, that was to enable easy extensibility into the Visual Studio product. In the end the engine that drive’s MEF is not so different other frameworks such as Unity, Ninject, StructureMap to name a few; they all manage registered objects and give them to you when requested. The differentiating part is how those objects are registered and how they are asked for.
The most interesting (I can’t already do it) portion of MEF is its ability to hand out objects simply by picking up assemblies at runtime. So to give it a spin I decided to create a simple MVC3 catalog application. Now I have been a fan of the NoSQL options for awhile now, so I decided that using a framework that doesn’t know what its going to do sounds like a great pair for a database that doesn’t know what its going to be given.
I will be using MongoDB for this setup, if you are unfamiliar it is simply a document database that stores its data in JSON. It holds no schema or expectations of what you will give it so it will require zero setup. If you attempt to run my code locally there are plenty of primers that can get you up and going with a local install in under 5 min.
The full project is on GitHub at https://github.com/brianwigfield/Extensible-Catalog. This is working code but I would not call it production quality, so rob and deploy at your own risk.
Scenario:
You have an application that maintains a list of consumer items that have many different sets of attributes depending on the type of item it is. You need to build an ecommerce application that is capable of displaying all of your different types of products and can be extended with more products very easily. You are subscribing to the CQRS model and decide instead of creating it to read the catalog data directly from the maintenance application you will have a process to ETL the data into a format for ecommerce. Since the ETL process is well beyond the scope of this post we will be picking up at the point we need to provide the interface over the prepared data.
Step 1
Since we are working with MVC3 we will build a dependency resolver that utilizes MEF to resolve MVC requests (controllers etc…).
[PartCreationPolicy(CreationPolicy.Shared)]
public class MefDependencyResolver : IDependencyResolver
{
const string Key = "MefContainer";
private readonly ComposablePartCatalog[] _catalogs;
public MefDependencyResolver(params ComposablePartCatalog[] catalogs)
{
_catalogs = catalogs;
}
protected CompositionContainer Container
{
get
{
if (HttpContext.Current == null)
return null;
if (!HttpContext.Current.Items.Contains(Key))
{
foreach (var catalog in _catalogs.Where(catalog => (catalog as DirectoryCatalog) != null))
{
((DirectoryCatalog)catalog).Refresh();
}
try
{
HttpContext.Current.Items.Add(Key,
new CompositionContainer(new AggregateCatalog(_catalogs)));
}
catch (Exception ex)
{
//TODO for some reason the add will toss duplicate key error the first time when running after dropping in a new assembly in the part catalog regardless of the contains check returing false
}
}
return (CompositionContainer)HttpContext.Current.Items[Key];
}
}
public object GetService(Type serviceType)
{
var exports = this.Container.GetExports(serviceType, null, null);
return exports.Any() ? exports.First().Value : null;
}
public IEnumerable<object> GetServices(Type serviceType)
{
var exports = this.Container.GetExports(serviceType, null, null);
return exports.Any() ? exports.Select(e => e.Value).AsEnumerable() : Enumerable.Empty<object>();
}
}
This code is not to complicated it takes an array of catalogs and searches through them when asked to resolve a service type, storing in the HttpContext here so we can reuse it for the request lifetime. It is going to rebuild the CompositionContainer and refresh any DirectoryCatalogs for changes for each request (this should be improved to have the MvcApplication attach directory watchers to do this only when needed).
Step 2
Now that we have the resolver we need to register it with MVC, in your Application_Start you will need to place this simple code.
var itemDirectoryCatalog = new DirectoryCatalog("CatalogItems");
var assemblyCatalog = new AssemblyCatalog(typeof (MefDependencyResolver).Assembly);
DependencyResolver.SetResolver(new MefDependencyResolver(itemDirectoryCatalog, assemblyCatalog));
We are adding the drop directory for the extension points and the current assembly, it will load any thing in the catalogs marked with [Exports].
There is also a few lines to let know MongoDB know about the serializable types, if your interested check out the source.
Step 3
Lets make some catalog items! I have created a ComplexItem and a SimpleItem project and preloaded my data with a few dummy items.
Here is an example of the preloaded data, pretty boring but you will notice the _t field which is utilized by the C# driver for MongoDB when it saves and loads the data. It is used as a discriminator value, which you may have used in nHibernate before for polymorphic table mappings, basically since we are storing multiple types of items in one collection it assists the deserializer in choosing which class to map the document to.
Now in code this is what the complex item looks like
[Export(typeof(CatalogItem))]
public class ComplexItem : CatalogItem, ListableItem, ItemDetail
{
public ItemListView ListView()
{
return new ItemListView()
{
Id = Id.ToString(),
Name = Name,
Price = Price.ToString(),
Description = Description
};
}
public VirtualFile DetailView(string virtualPath)
{
return new ExtensibleCatalog.Common.VirtualFileFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("ComplexItem.Detail.cshtml")
, virtualPath);
}
public void FillViewBag(dynamic bag)
{
bag.Name = Name;
bag.Sizes = Sizes;
bag.Styles = Styles.Select(x => x.Name);
}
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public IEnumerable<string> Sizes { get; set; }
public IEnumerable<Style> Styles { get; set; }
}
I am inhering from the base class CatalogItem which is defined in the main project which serves two purposes, first I defined the required MongoDB ID type and second to use when asking for types from MEF (which is noted by the type in the export attribute). This Item also implements the interfaces ListableItem and ItemDetail which allow it to be displayed in list results and show a detail page. The DetailView returns a virtual file which will allow me to embed a custom razor view in the ComplexItem assembly to be used by the main catalog application. I will spare you the SimpleItem details as it much of the same code.
Step 4
To display the items I have created an ItemsController in the application that has an Index and a Detail action. The controller itself is decorated to have its dependencies injected with MEF.
[Export, PartCreationPolicy(CreationPolicy.NonShared)]
public class ItemController : Controller
{
MongoRepository _repo;
[ImportingConstructor]
public ItemController(MongoRepository repo)
{
_repo = repo;
}
}
The controller class is decorated with the Export attribute, this is needed so that MEF will be able say that it has an ItemController when the DependencyResolver asks for one. The ImportingConstructor tells MEF that you expect it to inject the constructor params.
Now to add a property for MEF injection of the current CatalogItems goes just like this
[ImportMany(AllowRecomposition = true)]
public IEnumerable<Models.CatalogItem> Items { get; set; }
The ImportMany tells MEF to supply this property and the AllowRecomposition tells MEF that its ok if the known composition changes and to update this property (without this flag and the composition changes a rejection exception is thrown).
Now that we have the controller instantiated we need to add code to display a list of the catalog items
public ActionResult Index()
{
var loadedItemNames = Items.Select(x => new BsonString(x.GetType().Name)).ToArray();
var items = _repo.Items.FindAs<Models.CatalogItem>(Query.In("_t", loadedItemNames));
var viewData = items.Cast<Models.ListableItem>().Select(x => x.ListView());
return View(viewData);
}
First it needs to get the names of all the item types that MEF has registered, this is not absolutely necessary but if you end up selecting a record that you don’t have the class registered for then you will end up with a deserialization exception from the MongoDB C# driver. Next it uses those to query for results and then further filter by types that support the ListableItem interface (through more complex discriminator values you can query this at the DB level but I will leave that up to you). Then finally it returns the data to a strongly typed razor view for IEnumerable<Models.ListableItem>.
To support the details view it is a bit easier and a bit more complicated, first the code…
public ActionResult Detail(string id)
{
var item = _repo.Items.FindOneAs<Models.CatalogItem>(Query.EQ("_id", new ObjectId(id)));
return View(item.GetType().Name, item as Models.ItemDetail);
}
The query itself is very straight forward, you asked for an ID and it looks it up. Next it calls a small custom View method
private ActionResult View(string name, Models.ItemDetail details)
{
if (details == null)
return HttpNotFound();
else
{
details.FillViewBag(ViewBag);
return View(name);
}
}
If the requested item does not support details then you get a 404 response, other wise it populates the ViewBag and returns a view by the same name as the item type. There is a bit more magic under there that goes on that I won’t dive into, but the application has a custom VirtualPathProvider class that catches the request for the view and serves up the one supplied by ItemDetail.DetailView.
Step 5
Fire it up and give it a try! The first time you run it and click on Catalog hopefully you are not to surprised by a empty list of items.
We need to grab the ComplexItem.dll & SimpleItem.dll from their bins and drop them in to /CatalogItems/. To start just copy the SimpleItem.dll & .pdf then hit refresh… voilà it should show you your simple items with no recompile involved.
And copy the ComplexItem assembly over….
And for the final piece if you click on the ‘A complex jacket’ you should get your details page rendered from the embedded razor view…
Conclusion
I showed a working example showing the the injection of objects both from assemblies in a folder and from the assembly itself, but that doesn’t mean I am sold. For what a traditional DI framework can’t do it manages to do what it should, dynamically add extension points with no extra configuration. However for the traditional wire up’s that you normally do it is much messier than other options, you have to pre-decorate your classes with framework specific attributes which lets your IoC implementation muddy your domain code.
If you know what your dependencies will be at run-time then use what you have been, if you don’t then MEF is probably a good fit. Have both requirements in your app? Then use them both.
Comments
Tell us what do you think.
Trackbacks
Websites mentioned my entry.
There are no trackbacks on this entry
This stuff doesn’t wok.
1. What is CatalogItems?
2. How does MVC know that it should defer to your importing constructor in ItemsController?
CatalogItems is a DirectoryCatalog so it is looking for a directory in your project named ‘CatalogItems’. It looks like the blank directory did not get added to GitHub. Just create the directory and it will fix your issue.
MVC is forced to use that constructor because it is the only constructor on that controller.