Using a custom container to add default value support to MEF
The Managed Extensibility Framework does a great job of making it easy to write applications which are composed of discovered parts. One aspect of composing your application is that you can never quite know (unless its a private application where you have full control) which parts that will be available to the application, or even if there will be any parts at all. The latter is very important, you need to understand this so that you can build you application so that it doesn’t fail if an import cannot be satisfied, i.e. no matching exports were discovered.
Exactly how you build your application to handle when an import does not get satisfied is probably going to be on a case-by-case scenario, but the two most obvious approaches would be to either build in safe-guards to check for invalid import state (such as null value checks) or ensure that default values will be assigned to the imports if no exports were discovered.
MEF offers many different approaches to implementing support for default values. One approach is to take advantage of how MEF queries the export provider topology which gives you the possibility to implement different types of default value behavior (for example of the defaults should be overridable or not). I know the MEF team is planning to blog about this soon so I’ll not go into any details, but you should read Using ExportProviders to customize container behavior Part I by Glenn Block, Program Manager on the MEF team, for more information on ExportProviders and how they can be used to add custom functionality to the composition process.
As the title of this post suggestion, we are going to focus on how you can add support for default values using a custom container and a fluent interface. The following acceptance tests gives you an idea of what we are going to be building.
[Test]public void DefaultValueShouldBeUsedIfNoMatchingExportsAreFound(){ ContractBasedImportDefinition definition = new ContractBasedImportDefinition( "MessageService", ImportCardinality.ZeroOrOne, true, false); var defaultMessageService = new DefaultMessageService(); var container = new DefaultValueContainer(); container .Register("MessageService") .With(defaultMessageService); var results = container.GetExports(definition); Assert.IsTrue(results.Count() == 1); Assert.IsTrue(results.First().GetExportedObject().GetType() == typeof(DefaultMessageService)); Assert.AreSame(results.First().GetExportedObject(), defaultMessageService);}[Test]public void DefaultValuesShouldNotBeUsedIfMatchingExportsAreFound(){ FakeExportProvider provider = new FakeExportProvider(); provider.AddExport("MessageService", new SmtpMessageService()); ContractBasedImportDefinition definition = new ContractBasedImportDefinition( "MessageService", ImportCardinality.ZeroOrOne, true, false); var defaultMessageService = new DefaultMessageService(); var container = new DefaultValueContainer(provider); container .Register("MessageService") .With(defaultMessageService); var results = container.GetExports(definition); Assert.IsTrue(results.Count() == 1); Assert.IsTrue(results.First().GetExportedObject().GetType() == typeof(SmtpMessageService));}The two acceptance tests verifies that the correct default value behavior is applied by the container. The first test ensures that the default value is returned when no exports could be located (notice that the container is not passed any catalog or export providers when it’s created so it has no way of finding any exports) and the second test ensures that the matching export (you can read more about the FakeExportProvider class in my previous post Creating a FakeExportProvider to help with testing MEF code) is returned instead of the available default value. The second test highlights an important aspect of the container behavior; the default values and discovered exports are mutually exclusive, meaning they will never be returned at the same time.
Implementing the container
The first thing that we need to do is to derive a new class, the DefaultValueContainer class, from the CompositionContainer (which can be found in the System.ComponentModel.Composition.Hosting namespace, don’t forget to add a reference to the System.ComponentModel.Composition assembly that you can download at the MEF project site).
/// <summary>/// Defines a <see cref="CompositionContainer"/> with the ability to register default/// values for a contract./// </summary>/// <remarks>/// The default values will only be returned if no matching exports was found for the/// import./// </remarks>public class DefaultValueContainer : CompositionContainer{ /// <summary> /// Gets a list of <see cref="Export"/> objects which matches the provided <see cref="ImportDefinition"/>. /// </summary> /// <param name="definition">The <see cref="ImportDefinition"/> to get the exports for.</param> /// <returns>An <see cref="IEnumerable{T}"/> object.</returns> protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition) { throw new NotImplementedException(); }}The GetExportsCore method, of the CompositionContainer class, is where we will later add the logic for returning the default values if the container didn’t discover any matching exports. We need to take care of a couple of things before we implement the GetExportCore method, first of all we need a place to store the default values that have been associated with a specific contract.
/// <summary>/// Gets or sets the default values./// </summary>/// <value>A <see cref="Dictionary{TKey,TValue}"/> object,.where the key is the contract name and the value is the default value collection for the contract.</value>[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Multiple default values need to be able to be stored for a contract.")][SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Derived instances should be able to write to this property.")]protected Dictionary<string, Collection<Export>> Defaults { get; set; }It might look complicated but its not. It’s a normal C# auto-property with some FxCop rule suppressions added. The Default property is a dictionary which uses the contract name as the key and stores the default values in a collection. As the property declaration suggests, we are going to be storing Export objects instead of directly storing the default values. This makes sense since the GetExportCore method returns an IEnumerable of Exports, so instead of wrapping the default values right before they are returned we are going to wrap them when they are added to the container.
The second thing that the container needs are a set of constructor overloads. At a minimum the container should support the same overloads as the CompositionContainer class so that it makes easier to swap out the normal container with the DefaultValueContainer.
/// <summary>/// Initializes a new instance of the <see cref="DefaultValueContainer"/> class./// </summary>public DefaultValueContainer() : this((ComposablePartCatalog)null){}/// <summary>/// Initializes a new instance of the <see cref="DefaultValueContainer"/> class, using/// the provided list of <see cref="ExportProvider"/> instances./// </summary>/// <param name="providers">A list of <see cref="ExportProvider"/> instances.</param>public DefaultValueContainer(params ExportProvider[] providers) : this(null, providers){}/// <summary>/// Initializes a new instance of the <see cref="DefaultValueContainer"/> class, using/// the provided <see cref="ComposablePartCatalog"/> and list of <see cref="ExportProvider"/> instances./// </summary>/// <param name="catalog">The <see cref="ComposablePartCatalog"/> to add to the container.</param>/// <param name="providers">The list of <see cref="ExportProvider"/> instances to add to the container.</param>public DefaultValueContainer(ComposablePartCatalog catalog, params ExportProvider[] providers) : base(catalog, providers){ this.Defaults = new Dictionary<string, Collection<Export>>();}Now that the basic functionality have been added to the container we can turn our attention to writing an implementation of the GetExportsCore method. It’s inside this method that the default-value behavior is going to be implemented. The base implementation of this method already contains the code to try and retrieve a list of discovered exports which matches the provided import definition, so all we need to do is to add the code which should be executed when no matching exports was returned by the base implementation.
/// <summary>/// Gets a list of <see cref="Export"/> objects which matches the provided <see cref="ImportDefinition"/>./// </summary>/// <param name="definition">The <see cref="ImportDefinition"/> to get the exports for.</param>/// <returns>An <see cref="IEnumerable{T}"/> object.</returns>protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition){ var exports = this.TryGetExportsCore(definition); if (exports.Count() == 0) { var contractDefinition = definition as ContractBasedImportDefinition; if (contractDefinition != null) { string contractName = contractDefinition.ContractName; if (this.Defaults.ContainsKey(contractName)) { return this.Defaults[contractName]; } } } return exports;}The implementation is very straight forward; the base class implementation is queried for matching exports and if any were discovered then they will be returned. In the event of no matching exports the internal collection of default values (remember they will be wrapped in export objects) will be checked to see if it contains any exports which could be returned instead. The TryGetExportsCore method is part of the container implementation and is responsible for calling the base class implementation of the GetExportCore method and in the event that no matching exports were discovered it will catch the CardinalityMismatchException that usually is thrown and instead returns an empty list of export objects.
/// <summary>/// Attempts to retrieve exports matching the provided <see cref="ImportDefinition"/>./// </summary>/// <param name="definition">The <see cref="ImportDefinition"/> to get the exports for.</param>/// <returns>An <see cref="IEnumerable{T}"/> object containing the matched if any was found; otherwise <see cref="Enumerable.Empty{TResult}"/>.</returns>protected IEnumerable<Export> TryGetExportsCore(ImportDefinition definition){ try { return base.GetExportsCore(definition); } catch (CardinalityMismatchException) { } return Enumerable.Empty<Export>();}Adding the fluent interface for manipulating default values
The adding and removing of available default values are going to be done through a fluent interface on the container. The interface will support registering a contract for default value support, adding default values for the contract and to unregistering a contract. The first thing we are going to have to be able to tell the container that we want it to support default values for a specific contract. To do this we are going to implement two overloads of the Register method.
/// <summary>/// Registers a contract so that default values can be assigned./// </summary>/// <param name="contractName">Then name of the contract to register default values for.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <exception cref="ArgumentException">The provided value of the <paramref name="contractName"/> parameter was <see langword="null"/> or empty.</exception>public DefaultValueContainer Register(string contractName){ if (string.IsNullOrEmpty(contractName)) { throw new ArgumentException("The name of the contract cannot be null or empty."); } if (!this.Defaults.ContainsKey(contractName)) { this.Defaults.Add(contractName, new Collection<Export>()); } return this;}/// <summary>/// Registers a <see cref="Type"/> as a contract so that default values can be assigned./// </summary>/// <param name="contractType">The <see cref="Type"/> to register as the contract.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <remarks>The value of the <see cref="Type.FullName"/> property will be used as the contract.</remarks>/// <exception cref="ArgumentNullException">The provided value of the <paramref name="contractType"/> parameter was <see langword="null"/>.</exception>public DefaultValueContainer Register(Type contractType){ if (contractType == null) { throw new ArgumentNullException("contractType", "The contract type cannot be null."); } return this.Register(contractType.FullName);}Implementing so that you can instruct the container that you no longer want it to add default value to a specific contract is just as simple. We do this by adding an Unregister method and provides the same two overloads as we did with the register method. Unregistering a contract will delete all records of it, including the associated default values, from the container,l
/// <summary>/// Unregisters a contract and removes all previously registered default values assigned to it./// </summary>/// <param name="contractName">The contract to unregister.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <exception cref="ArgumentException">The provided value of the <paramref name="contractName"/> parameter was <see langword="null"/> or empty.</exception>public DefaultValueContainer Unregister(string contractName){ if (string.IsNullOrEmpty(contractName)) { throw new ArgumentException("The name of the contract cannot be null or empty."); } this.Defaults.Remove(contractName); return this;}/// <summary>/// Unregisters a contract and removes all previously registered default values assigned to it./// </summary>/// <param name="contractType">The <see cref="Type"/> to extract the contract name from.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <remarks>The value of the <see cref="Type.FullName"/> property will be used as the contract.</remarks>/// <exception cref="ArgumentNullException">The provided value of the <paramref name="contractType"/> parameter was <see langword="null"/>.</exception>public DefaultValueContainer Unregister(Type contractType){ if (contractType == null) { throw new ArgumentNullException("contractType", "The contract cannot be null."); } this.Unregister(contractType.FullName); return this;}The final part of the fluent interface is to be able to add the actual default values. This is done with the help of the two overloads of the With method. The With method is responsible for associating the provided default value(s) with the contract that was last registered with the container. As previously mentioned we are going to wrap the default values in export objects when they are added to the container, making the With method the perfect place to add this functionality.
/// <summary>/// Adds a default value to the last registered contract./// </summary>/// <param name="defaultValue">The default value to add.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <exception cref="ArgumentNullException">The provided value of the <paramref name="defaultValue"/> parameter was <see langword="null"/>.</exception>public DefaultValueContainer With(object defaultValue){ if (defaultValue == null) { throw new ArgumentNullException("defaultValue", "The default value cannot be null."); } Export wrapper = new Export(this.Defaults.Last().Key, () => defaultValue); this.Defaults.Last().Value.Add(wrapper); return this;}/// <summary>/// Adds a collection of default values to the last registered contract./// </summary>/// <param name="defaultValues">The collection of default values to add.</param>/// <returns>The current <see cref="DefaultValueContainer"/> object.</returns>/// <exception cref="ArgumentNullException">The provided value of the <paramref name="defaultValues"/> parameter was <see langword="null"/>.</exception>public DefaultValueContainer With(IEnumerable defaultValues){ if (defaultValues == null) { throw new ArgumentNullException("defaultValues", "The default values cannot be null."); } foreach (var defaultValue in defaultValues) { this.With(defaultValue); } return this;}And that’s really all there is to it! We’ve now implemented a custom composition container which supports mutually exclusive default values for imports that could not be satisfied with the help of discovered parts. The DefaultValueConainer will have the exact same behavior for imports, of contract that has not been registered for default value support, as the CompositionContainer class which is shipped with MEF, making it very easy to retrofit existing composition code that you may have in place.
How the container could be further extended
I could probably make this post two or three times the length if I covered some of the ideas I have on how the DefaultValueContainer class could be improved even more, but I’ll leave that up to you so you can tailor it for your exact specific needs.
Some of the ideas I’ve had along the way are
- Constructor overload for injecting defaults when the container is instantiated
- Extending the fluent interface to be able to control if defaults for a specific contract should be mutual exclusive or if a union consisting of the default values and discovered exports should be returned
- Being able to unregister individual defaults on a specific contract
- Adding support for notification events. Perhaps change the implementation of the defaults value so that it uses an ObservableCollection instead
Finally
You can download the full source code, including a suit of acceptance tests, at my codeplex.com page. There is a good chance that an implementation of this class will end up in the trunc of the MEF Contrib project, an open-source contribution project for the Managed Extensibility Framework, in the future so keep an eye on the project. If you would like to join the MEF Contrib project, please let us know!