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.
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:
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:
- 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] :
The one-to-one dependency between ProductLookup and ProductRepository correspond to MEF required imports. The same applies to ServiceLookup and ServiceRepository:
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:
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:
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.
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?)