Implementing Optional Exports with MEF Stable Composition

Disclaimer: As usual, this blog post is discussing pre-release software, which may differ from the final released version.

Ayende once observed that MEF is very focused on dependency management. It’s an accurate description of the driving force that has shaped MEF as it is today.

Dependency management doesn’t end at build time with assembly references. Functional dependencies are more complex and harder to control than static compilation unit dependencies. MEF has some nifty tricks up its sleeve to help out with this task.

Imagine the example of a retail management system:

Standard

Pro Service

Platinum

Service Schedule

X

X

Stock Control

X

X

Client List

X

X

Marketing

X

The AdventureWorks Store Manager application has many features. To support pay-for-play licensing, the company sells several versions (‘SKUs’) targeting different market segments, with different features enabled.

All of these versions are subsets of the same essential codebase. Each SKU, or configuration, includes the relevant pieces.

Mapping Features to Components

The implementation of each feature can involve more than one component. Part of the system might look like:

image A quick description:

  • PointOfSale lets a storeperson process sales
  • PointOfSale uses a collection of ItemLookups representing each kind of thing that can be sold
  • ProductLookup provides ItemLookup functionality for products, and requires exactly one ProductRepository to do its job
  • ServiceLookup is like ProductLookup but for services that may be performed for the client

An important aspect of the diagram is the cardinality of the relationships.

PointOfSale’ s one-to-many dependency on ItemLookup is a MEF [ImportMany] :

image

The one-to-one dependency between ProductLookup and ProductRepository correspond to MEF required imports. The same applies to ServiceLookup and ServiceRepository:

image To configure the system, we need to make sure that ServiceLookup doesn’t appear in PointOfSale.ItemLookups when we’re in the “Standard” SKU.

Partitioning the System

‘Classic’ approaches to sub-setting used to revolve around conditional compilation ( #ifdefs) or runtime “IsEnabled” checks. While effective, the results of these methods can be very difficult to maintain.

Plug-in systems, or selective registration of components in an IoC container, provide a more intentional, centralized way of managing the contents of a SKU.

Even here there is significant complexity and thus room for improvement. Functional dependencies usually span layers and revolve around multiple pieces of code. Some components will appear in multiple SKUs, while others will be available with different degrees of functionality.

(Note that we ignore assembly-level partitioning here since market-driven functional partitioning does not always match architecture-driven partitioning.)

Enter Stable Composition

Tucked away in the recent announcement of MEF preview 6 was a snippet on what we’ve been calling Stable Composition.

The cornerstone rule of Stable Composition is:

Parts that have missing required dependencies are ‘rejected.’ They appear in the catalog, but the container never uses them.

Now, the impact this has on a system as a whole is more interesting than it would first appear. Not only will parts with missing dependencies be rejected, but so will any parts that transitively depend on rejected parts. One missing dependency can cause a whole graph of components to be ignored.

This isn’t accidental:

Utilising Stable Composition, one can manage just the parts that define a SKU, and MEF will make sure that dependent parts are included or excluded as needed.

Instead of tracking dependency relationships manually to create a master set of components for each SKU, offload this responsibility to MEF.

Configuring AdventureWorks Store Manager Standard SKU

This SKU is characterised by the absence of service-related functionality.

Instead of analyzing all components to determine whether they play a part in service-related scenarios, we make the observation that all such functionality depends on the presence of a ServiceRepository.

When the MEF ComposablePartCatalog is being configured, excluding the ServiceRepository gives us the result we require.

MEF itself will determine which other components depend on the missing functionality and automatically ignore (‘reject’) them:

imageThis behaviour comes out-of-the-box with MEF – there is no need to opt-in or change the way that the CompositionContainer is used.

Single Required Dependencies Only

Rejection typically applies to one-to-one dependencies. The ServiceLookup screen is rejected because it requires exactly one ServiceRepository, and by excluding it from the SKU we’ve effectively disabled the ServiceLookup screen.

The PointOfSale screen’s dependency on many ItemLookups will never cause it to be rejected, since it can effectively function when the number of available ItemLookups is zero.

The other common MEF dependency, represented by an import with AllowDefault=true, is an optional dependency. Since by declaring an optional dependency, a component guarantees that it can operate without the dependency present, optional dependencies do not trigger rejection either.

The special case to look out for is when too many implementations are present, e.g. two ServiceRepositorys. In this scenario components that depend on a single instance will be rejected.

Debugging and Testing

The last drop of MEF included a sample (Microsoft.ComponentModel.Composition.Diagnostics) capable of determining the rejection status of a part. This is useful for debugging at development time, but there’s also an opportunity to do integration-time testing of SKU configurations.

For example, if I’m testing the “Pro Service” SKU and expect ServiceLookup to be available, I might write:

image

Now, this is a hypothetical test case; I haven’t had an opportunity to use this in anger. It is an interesting possibility though, and worth exploring if you’re going to make heavy use of Stable Composition.

Optional Exports

Why did the title of this post include the term ‘Optional Exports*’? This is a useful way to think about how rejection works. MEF components provide exports that are optional, on the condition that their mandatory imports can be satisfied. The ServiceLookup component provides its exports on the condition that ServiceRepository is available.

You can use this way of looking at the feature to implement some other nifty tricks in the realm of ‘light up,’ but that is for another day!

(*One of the many subtle coinages of Blake Stone – Blake, where’s your blog?)