A while ago I said it couldn’t be done, at least without hacky string parsing. Folks weren’t happy and they let us know it including Oren. Our team know the power that open-generic support brings to systems, but at the time there was no clean way to implement it in MEF. Then a bunch of time passed, and we actually added some APIs to MEF which suddenly made achieving support a reality (thanks to Wes for pointing the path), and not in the hacky string-parsing way I had described.
It all came together a few weeks ago after I was pairing with my friend Karl Shifflett on the Cider team. We ran into an issue around usage of MEF which really needed open-generics, and without it made me question the usage of MEF for that scenario. As I have a tendency to do, I then set off on a personal mission to find a solution. And now several weeks and light nights later, and after quite a bit of help from many of my teammates I am happy to say there is one! I am a tech geek so I usually now take the opportunity to dive into all the nitty gritty of what what the implementation is. This time however, I am going to change that tune and only focus on what it is, how you use it and how you can get it (now).
How can I get it?
If you want to get right to the code, then GenericCatalog ships as part of MEF Contrib. I’ve uploaded two projects MefContrib.Extenions.Generics and MefContrib.Extensions.Generics.Tests. You can go grab the source on contrib here. Andreas is shortly releasing an official release of MEF Contrib, which will contain this in binary form. If you want the binary, tweet Andreas and say I want MEF Contrib!
What is it?
Open-generic support is very simple. It means that you can provide a part with a generic contract, that can be used to satisfy all imports of a closed form of that contract. The canonical example folks use is Repository<T> and IRepository<T>. What I want to be able to do is register a generic Repository such that any imports of IRepository<T> can be satisfied, this IRepository<Customer>, IRepository<Order>, etc can all be handled by a single Repository<T> that is registered in the catalog.
MEF’s attributed model however does not support this, we don’t allow even exporting open-generic types. If you put an export attribute on say Repository<T>, we ignore it. MEF does support closed generic types. For example I can have an importer of IRepository<Customer>, and i can register Repository<Customer> which exports IRepository<Customer>. However, that means that I have to add a specific implementation of IRepository<T> for every repository in existence. This is problematic because the importer doesn’t want to be burdened with having to add these specific implementations, or to know whether or not it even exists.
The new GenericCatalog changes that. Generic Catalog is a custom catalog that can both discover open-generic implementations, and create closed implementations on demand. It also supports generic specialization, that is it allows you to register specific implementations of a generic contract, which override the default that will be created by the open-generic. Finally it supports one other requested feature, that is it can create concrete instances that are imported, even if they were not added to any catalog.
GenericCatalog is a decorator. You pass it in it’s constructor all your catalogs, and it sits on top, delegates to the inner catalogs, and intercepts requests for generic types that were not found in the catalog.
How you use it
To see how it works, check out the specification / context below (otherwise known as a unit test)
Looking at the spec we can see that we are creating the container passing in a generic catalog. We are then asking the container for an IRepository<Order> and we are verifying that we got one.
Notice in the test that we are calling the overload that accepts an ImportDefinition rather than just calling container.GetExportedValue<IRepository<Order>>. MEF creates special kinds of ImportDefinitions when you add an Import to a part. These definitions carry additional information that the generics implementation relies on. When you call GetExport directly on the container however, the definition that is created is a different definition which doe snot carry this information. As such, in order to take advantage of the new functionality, you import the generic type in a part. For example, the IRepository<Order> definition came from this import below.
OrderProcessor is importing IRepository<Order>
How do you setup the container?
In order to setup the container to support open-generics, you create the GenericCatalog passing in all your other catalogs, usually this will be your conventional AggregateCatalog that contains all your catalogs today. For example below in the base context class you can see how we setup the catalog for this test.
In this case we are creating a type catalog that we are adding our types for our test, an OrderProcessor and a RepositoryTypeLocator (more about that in the next section). Next we are creating an AggregateCatalog, and adding the type catalog to it. Finally we are creating a GenericCatalog and passing it the Aggregate which contains everything else. Next I do a bit of hackery to get the ImportDefinition off of the OrderProcessor in order to do the query in the test. As i mentioned you shouldn’t have to do this, as you’ll be grabbing something from the container that likely depends on the generic import rather than needed it directly.
If you are following along, you might be asking yourself where are the open-generic types? And that is where GenericTypeMapping comes in. As I mentioned earlier, MEF does not allow exporting / importing open-generics. To work around that, I’ve introduced a non-generic contract that carries generic type information 🙂 Not only that, but the implementation types passed in actually are open-generic parts!
GenericTypeMapping accepts two parameters in it’s constructor, one is an open-generic contract type, and the other is an open-generic implementation type. This type also is an inherited export, taking advantage of our new Preview 6 feature which thus removes the need for the attribute on derivers.
To use it, you derive from GenericContractTypeMapping for each open-generic type you want to export. You then make sure that it is in one of the catalogs that is passed in to the GenericCatalog. In our example we have a RepositoryTypeLocator which has a contract of IRepository<> and an implementation of Repository<>.
Repository<> is a generic part. It supports constructor injection, can have imports / exports just like any other part.
So all you have to do is create generic parts, and corresponding type mappings, put them in the catalog, and as Karl Shifflett says, “DONE!”
How it works (You don’t have to read this)
GenericCatalog is what is doing all the magic here. This guy automatically queries his inner catalog for all GenericTypeMapping contacts. Once he has them, he takes the types within and adds them to a mapping table from generic contract to generic implementation. Whenever the catalog is queried, it will see if any exports were returned, if not and the importing type is generic, it will grab the generic type definition, and lookup in that table built earlier. If it finds that it can match against that definition, it will grab the implementation and create a closed generic type. It will then add the new type to a TypeCatalog, which it will add to it’s inner catalog. Once it does this, it then queries the catalog to grab the new export, returns it, and Voila.
You can dig into the source if you want to know more of the nitty gritty.
Will this ship as part of MEF V1?
No. Actually being brutally honest, the implementation here would probably never ship as part of the framework. This is an additive approach to solving the problem of open-generics, but it is not the approach we would choose to ship in the framework. We are looking seriously into baking such support into MEF in the future, but it will not be in V1. If and when we do, I guarantee it will be a much cleaner and deeply integrated part of MEF as opposed to what I am giving you. However, I believe as do others on my team that in the meanwhile, this approach is reasonable. We’ve put this in MEF contrib to ensure that the community can take it forward from here.
We appreciate any feedback. I take full responsibility for any hackiness you find within. :p
Special thanks to all my awesome teammates and to Andreas for help in getting this out the door.