次の方法で共有


Customizing container behavior part 2 of N - Defaults

Before I get into the post, let me start off with a word of advice for new bloggers. Never say in a post, this is the first of many to come. RESIST! You are just setting yourself up for never doing that second post. I know as the last post in this series was five months ago. ;-)

In the last post, I introduced the concept of ExportProviders as a way to customize how the CompositionContainer discovers exports. One of the primary use cases for doing this is to provide default exports, which is particularly important for cases where you are importing a single value for which there are multiple present. The common case I like to use is the overused Logger example. Let’s say you have multiple loggers in the system, which are used for different functions. One logger logs locally, while others can log remotely perhaps invoking a web service, or using MSMQ. These loggers are all pluggable, as different organizations that use the app may have different needs. Each of these loggers export an ILogger contract.

Now most of the time when logging occurs the local logger is sufficient. It’s only exceptions to the rule that require the special loggers. And this is where you run into a problem. How do you specify that you need to import that default logger, where there are multiple present? In the default configuration of MEF, the only way to really handle this if you are using imports is to either create a specialized contract for the default logger say IDefaultLogger which is imported, or to import the entire collection lazily using Export<ILogger> and then looking at the metadata for each to find the right one. You could also create a custom service called LoggerService, which imports all the loggers finds the default one, and then returns it when LoggerService.DefaultLogger is called. None of these are optimal though.

For example, you could do this….

 [Export]
public class LoggerManager : ILoggerManager
{
    [ImportingConstructor]
    public LoggerManager(Export<ILogger,ILoggerMetadata>[] loggers)
    {
        var logger = loggers.Single(e => e.MetadataView.IsDefault==true);
        DefaultLogger = logger.GetExportedObject();
    }
    
    public ILogger DefaultLogger { get; private set;}

}

The problem is this means the LoggerManager has to have hard knowledge of which logger is the default. It also means the logger itself needs to somehow declare that it is the default, which is problematic because one logger might be the default in one app, but not in another. Separation of concerns anyone? Even worse you might have two loggers that say they are the default!

Now there are other mental hoops you can jump through to find a workable solution, but it’s a lot of pain when what you really want you want to do is tell MEF’s container somehow “This is my default logger for this application”. Not only that but you want to do it in a way that does not require the app to hardcode it, does not require explicit configuration, nor does it require the parts to somehow ‘tell’ MEF they are the default. Also you don’t want the parts that are importing ILogger to have to know or care about a LoggerManager or anything like that. Instead they should simply have an import of ILogger.

 [Export]
public class Executor
{
    [ImportingConstructor]
    public Executor(ILogger logger)
    {
    }
}

The $25000 question is how? The answer is using our friendly ExportProvider. In my last post I described the role of ExportProviders, which is to simply return exports. Those exports can come from several different sources, including from catalogs.

Constructing the container with ExportProviders

If you look on the constructor of the container, you’ll see several overloads which accept an ExportProvider.

 public CompositionContainer(params ExportProvider[] providers);public CompositionContainer(ComposablePartCatalog catalog, params ExportProvider[] providers);  

Any export providers passed in to the container are incorporated into the container’s query strategy. This means whenever anyone pulls on the container for an export, these providers will also get queried. Now catalogs have a special provider associated with them called a CatalogExportProvider. As a matter of fact the overload on the container that accepts a catalog is really just syntactic sugar. Behind the scenes we immediately create a CatalogExportProvider passing in the catalog. This means you can pass in an additional default catalog. If you’ve worked with MEF before, you might be shaking your head at this point and thinking “How is this any different than just having an AggregateCatalog and adding two catalogs?” And that IS the key question ;-).

The answer is that adding catalogs to an AggregateCatalog does not allow handling defaults, however using this technique with ExportProviders does. I’ll explain how next.

AggregateCatalogs vs AggregateExportProvider

AggregateCatalog is a catalog that contains a list of catalogs. It is a composite pattern, so querying the parent, queries all the children. This is especially useful when you have parts that are “built in” to the application which are added via an AssemblyCatalog, and other parts which are discovered through a DirectoryCatalog. You can add both catalogs to an AggregateCatalog and everything works nicely. It doesn’t help you though in a default scenario however. The reason is because if one of your catalogs contains your default ILogger and your other catalogs contain the pluggable ILogger implementations, then the aggregate catalog will return them all when it is queried. This means the Executor above will blow up as the constructor is expecting a single ILogger.

For the AggregateExportProvider however, we designed specifically to allow this defaults scenario to be addressed. The provider behaves differently depending on whether it is being queried for a single export or multiple. If the query is for multiple exports, then it works similar to the AggregateCatalog and collects all the exports it finds in any of it’s providers. If on the other hand the query is for a single export, then it uses a prioritization scheme. It will start at the top of the list of ExportProviders and query the first one for the single export (Logger). If it does not find one, then it keeps querying on to the next ExportProvider and so on. Once it finds an ExportProvider that returns one, then it will be returned, if it does not find one, then it will throw an exception.

This means you can have default ExportProviders in the chain (including CatalogExportProviders) that contain your defaults so that the Executor class always succeeds in creation. You can even configure the behavior so that the defaults are overridable or not. For example if you want the default ILogger to always be the logger that is returned for all single import queries no matter what, then put the defaults at the top of the chain. If however you want the default logger to be overridable by another single ILogger, then put it at the end.

Here’s an illustration of what I mean.

 public void OverridableDefaults()
 {
     var catalog = new DirectoryCatalog(@".\Extensions");
     var defaultCatalogEP = new CatalogExportProvider(new DirectoryCatalog(@".\Defaults"));
     var container = new CompositionContainer(catalog, defaultCatalogEP);
     defaultCatalogEP.SourceProvider = container;
 }

In the configuration above, we are placing our defaults at the end of the chain of providers. Remember as  I mentioned above that the ‘catalog’ reference will get wrapped in a CatalogExportProvider as well. It will get added first, followed by the additional providers. We then have to set the SourceProvider to the container, I talked about why this is necessary today in my previous post, though it may be possible for us to infer the setting in the future. Anyway, now when a query happens for a single logger, the container will first check the parts in the Extensions folder, and if none are found it will revert to the defaults.

 public void NonOverridableDefaults()
 {
     var defaultCatalog = new DirectoryCatalog(@".\Defaults");
     var catalogEP = new CatalogExportProvider(new DirectoryCatalog(@".\Extensions"));
     var container = new CompositionContainer(defaultCatalog, catalogEP);
     catalogEP.SourceProvider = container;
 }

In this example, we are passing in the default catalog first, and then passing the catalog (as an EP). This configuration now means that the defaults are NEVER overridable, as whenever the container is queried for a single logger, it will ALWAYS return the default.

Using Type or Assembly Catalogs for defaults

Now in the approach above we are using DirectoryCatalogs for specifying defaults. But remember, this is an option, you don’t have to, and in many cases might not want to. For example you might want to just specify your list of defaults in code, or even in a config file. TypeCatalog provides a great way to do this. (It accepts a params of Types, so you can add as many as you want).

 public void OverridableDefaultsWithTypeCatalog()
 {
     var catalogEP = new CatalogExportProvider(new DirectoryCatalog(@".\Extensions"));
     var defaultCatalogEP = new CatalogExportProvider(new TypeCatalog(typeof (ILogger)));
     var container = new CompositionContainer(catalogEP, defaultCatalogEP);
 }

OK, so now that you sold me, what are the caveats?

As with all things, there are some. Really, I can only think of one primary one, and that is that there’s still a bit of indeterminism as to what will get returned on a single import depending on how you set up the provider chain. If you go with the approach of putting defaults first, then it is completely deterministic. You know that whenever a single import request is issued to the container, the exports in that catalog will be returned, period. If however you arrange the defaults at the end of the chain, then it really depends on what is in the middle. Let’s say you have 3 EPs, and the one in the middle has a part with the single export that you are looking for, while you also have a default. In that case the one in the middle will get returned. Another caveat might be that this does add a bit of complexity, however I would argue that’s a reasonable trasdeoff to having to import collections all over the place.

The code

You can download sample code that illustrates using ExportProviders in various fashions here

If you look inside, you’ll find much more than just what we covered here.  Including how to do do role based composition, or even wire MEF to a configuration store where config info is an import. All of this will get covered at some point.

Additionally:

  1. You’ll find several test classes which are using a context/specification style of testing to illustrate how to use ExportProviders.  If you are not familiar with BDD/CS it may seem very foreign, but I think you’ll find it easy to follow once the initial shock wears off. Having gone through this experience made me a big fan of CS style, and that is for another post.
  2. You’ll find several test helper classes I’ve written specifically for MEF. These are really useful for folks writing their own ExportProviders and who wish to mock / fake certain parts of our programming model. Specifically I am talking about FakeExportDefinition and FakeExportProvider. Additionally you’ll see some common classes I’ve used for my own CS experiments.

For stuff relating to what was covered in this post, go check out the Import_Logger_Specs.cs unit-tests.

Special thanks to Scott Bellware as well as Scott C. Reynolds and David Foley who invested a lot of time and effort in getting me off the ground in using this style of testing. Without their help, I would have been stranded in a pool of confusion. This post is really not doing CS justice, and I really need a separate series. I can’t guarantee that I’ll get to it, but I’ll try.

What’s next

In the next post we’ll talk about establishing parent-child containers hierarchies. You may be asking how does this relate to ExportProviders? I’ll leave you with this thought to ponder, our container IS an ExportProvider.

 

Comments