Using ExportProvider to customize container behavior Part I

In our last drop of MEF, one of the significant changes was the introduction of the ExportProvider. In this next series of posts, we'll take an in-depth look at what it is, how it's used by the CompositionContainer, and how you can use it to customize the behavior of the container.

Disclaimer: These are not intro to MEF posts, if you don't have a good grasp of MEF concepts, then these posts may be a hard to follow.  Please refer to the MEF Wiki on our Codeplex site, or to community posts (we have some bloggers links on our site) if you need further reference.

Just to give you a preview of what's to come, here are some of the scenarios you can address using Export Providers.

  1. Providing a filtered view over exports that come from other providers. This may be a fixed filter, or something more dynamic such as based on application state or a user's role.
  2. Providing default exports such as a logger which will get returned whenever a single logger is imported if there are multiple loggers present.
  3. Creating a hierarchical model with parent / child containers.
  4. Injecting configuration information into parts during construction.
  5. Integrating with legacy systems, or other component providers such as an IoC container.

ExportProvider

Parts in MEF carry exports and imports. During composition, the container composes parts and satisfies imports. In order to do this, it queries a series of export providers as can be seen in the diagram below.

image_thumb24

ExportProviders have one purpose, drum roll please. They provide exports. Where those exports come from is unimportant to the caller, the main thing is that the exports that returned match the request. 

In the box

As can be seen from the diagram above, we make heavy use of export providers with MEF, and we include several in the box. You can also create your own.

CompositionContainer - The container itself is an Export Provider. This comes in very handy when building topologies of containers such as Parent / Child, or even Two Parent / Child  (yes it is possible).

MutableExportProvider - This provider is primarily an implementation detail of the container. Whenever you manually add parts to the container, those parts are added to an internal part collection within the MutableExportProvider. Whenever the MutableEP is queried, it then queries its parts to find matching exports. It contains a CompositionEngine, which is responsible for satisfying the imports on any of the parts that are added to it.

AdaptingExportProvider - MEF supports contract adapters . This provider queries a set of exports, and then invokes an adapter manager to adapt the exports returned to a different contract. More on this in another post.

ComposablePartCatalogExportProvider - This provider is responsible for retrieving exports from part catalogs. Like the MutableExportProvider, it too contains a collection of parts. However the parts in it's collection are created from the PartDefinitions it queries rather than being explicitly added to it. Also similar to the MutableEP, it too contains a CompositionEngine for satisfying the imports on it's parts.

AggregatingExportProvider - This provider is a composite of other providers that it contains, and is used for providing a topology of EPs. Whenever this provider is queried, it will query the providers within. The internal query behavior varies depending on the cardinality of the ImportDefinition (more on that below) that is passed in.  For now it is sufficient to know that it queries it's children. We will talk more about the behavior in a future post. The container uses an AggregatingEP internally which contains a MutableEP, a CatalogEP and/or a custom provider if one was passed in during its construction. AggregatingEPs can also be nested without a problem.

Under the covers

If you take a look at the ExportProvider API you'll see the following.

image

At first glance you may be thinking, Wow that looks anything but simple. Majority of these methods are different ways for specifying a set of exports to retrieve, a format to return them, and whether or not it is a single item or a collection that is returned. The GetExport / GetExports methods return lazy instantiated instantiated objects which are of type Export.  The GetExportedObject / GetExportedObjects methods returns the actual instances that  the Exports create.

Fortunately about 95% of the methods are syntactic sugar around one core method which is the only method you need to implement when authoring a custom ExportProvider.

image

That method takes an ImportDefinition and returns a collection of Exports.

ImportDefinition

You can think of the ImportDefinition as similar to a SQL where clause. It  specifies a filter for which Exports to return. The ImportDefinition has two main components. The constraint is an Expression<Func<ExportDefinition, bool>> and represents the export filter. Cardinality is an enum which specifies the cardinality of the exports, it can be ZeroOrOne (one max, but zero is allowed), ExactlyOne, or ZeroOrMore ( a collection of 0 to N). We'll hold of on talking about the other params for now.

image

These definitions come from several places. Parts carry import definitions, for example when you decorate a Part with one or more Import attributes, it will have  ImportDefinitions created for each Import as it is picked up by the catalog. The ExportProvider has several public methods that accept definitions as parameters. As the container is an ExportProvider this means definitions may be passed in directly through it's methods. Finally, if any of the overloaded GetExport(s)/GetExportObject(s) methods  on the ExportProvider that do not accept an ImportDefinition are callled, internally an  ImportDefinition will be created.

For example, the snippet below illustrates creating a definition that matches on all exports that use the convention "Service" as a suffix.

image

This is a very simple constraint, but you can let your mind run wild as to what you can do through an expression.

ContractBasedImportDefinition

As you can see above, the ImportDefinition takes a constraint as an Expression. This is great for cases where we need to support free-form queries such as you can specify with a lamdba. However an expression is essentially opaque unless you want to do some deep parsing. Also expressions are overkill for the simple cases such as declarative imports and exports on a part. This is where the derived ContractBasedImportDefinition comes in. 

image

This definition allows you to specify both a contract name, and an optional set of metadata keys that should be matched on (keys not values).

Our attributed part model creates these definitions for each Export and Import attribute we find. This is ideal for many cases and it provides the added benefit of making it easier to author an ExportProvider as you don't have to work with expressions for 95% of cases.

Below is an illustration of creating a ContractBaseImportDefinition to match on a logger.

image

ExportDefinition

Earlier was saw that the ImportDefinition takes a constraint on an ExportDefinition. Whereas the ImportDefinition defines what kind of exports are needed, the ExportDefinition defines the actual export itself.

image

As you can see it takes two parameters. The contract and a dictionary of metadata.

Contracts are strings

You might be surprised at the fact that contract appears as a string though the ExportAttribute allows you to pass a type. Yes folks, MEF doesn't care about types, under the hood it's all strings. When you pass a type, that type's full name gets pulled out (without the assembly info) and this is the contract. This is a subtlety that actually indicates one of the real powers behind MEF. MEF imports can come from anywhere, Dynamic Languages, XAML, or even a database. The only thing that matters is that the Importer is of a compatible type to cast the Export. However that Export did not at all have to be retrieved based on it's type.

Where does the metadata came from?

Part exports carry metadata. In our attributed part model, metadata is specified in one of two ways. One way is by annotating an Export with an ExportMetadataAttribute, which contains a name, value pair. A second approach is to use a custom metadata attribute.  Below you can see InMemoryCache is exporting metadata defining that it is a non-persistent cache.

image

 

Passing in Export Providers

Export Providers are passed in to the container during its construction. There are two overloads on the constructor which accept an EP.

image

 

image

Behind the scenes, an AggregatingExportProvider is created and each of the providers that were passed in is added to it. If a catalog is passed in, then a CatalogExportProvider is created and it is added to the collection of providers. In other words the container doesn't know anything about catalogs, only Export Providers. If you don't want to use catalogs, you don't have to.

Source Provider

Some export providers need to have access to other EPs in the outside world. In particular these are the EPs that need to satisfy their imports, such as the CatalogExportProvider and MutableExportProvider. For example, when the CatalogExportProvider is queried for an export such as a ContactView, and that view imports a NotesView, how can the CompositionEngine find the NotesView. If it only looks within itself, then it won't find NotesView exports that may exist in another catalog, or in a different provider such as the MutableEP.

For this reason these providers have a property called SourceProvider which is set to a provider that gives them that access. 9 times out of 10, this property is either set to the container itself, or to the container's AggregatingEP. It is important to remember that when you create CatalogExportProviders yourself and pass them in to the container, you need to set the SourceProvider to the Container after the Container has been created.

What's next?

Now that we've got the basics down pat we can move on to seeing different ways we can author and use Export Providers. In the next post, we'll take a look at how to create a filtered EP. We'll also look at how to design EPs in a test-driven matter, including a few APIs I whipped up to aid in testing EP functionality using tools such as Rhino Mocks.