Поделиться через


Import Cardinality, and Picking Which Export to Use

In the Managed Extensibility Framework (MEF), an import has a cardinality, which expresses how many exports can be used to satisfy the import.  The possible values are ZeroOrOne, ExactlyOne, or ZeroOrMore, and they can be declared in the following ways:

 [Import(AllowDefault = true)]
IService ZeroOrOneImport { get; set; }

[Import]
IService ExactlyOneImport { get; set; }

//  Note that the property here is an array.
//  It could also be IEnumerable<IService> or a concrete type that implements ICollection<IService>.
[ImportMany]
IService[] ZeroOrMoreImport { get; set; }

If the cardinality of the import is ExactlyOne, then there must be exactly one matching export available.  If there are zero, or if there are two or more, then the import will not be satisfied, and the part will either be rejected or the composition will fail.

A common question we get about MEF is how to modify this behavior, either by specifying a default export to use, or by specifying some logic that will pick the best one according to some criteria.  This question recently came up internally at Microsoft, and our architect Blake Stone gave a good explanation and overview of the options.  The rest of this post is based on what he said.

In general, we strongly encourage consumer-side policy on picking one of many for what we believe is a good reason. We're trying to encourage not just individual stand-alone extensible systems, but the development of reusable subsystems. If two different teams develop something useful independently then we'd like to be able to drop both of them into a single product and have them work. Designing with a system-wide policy in mind prevents this from working. A natural workaround is to try to isolate the two subsystems from one another using different containers, but this also leads to problems down the road as subsystems tend to overlap in time - and it becomes awkward for one part to participate in more than one subsystem.

So the recommended approaches look like this:

a) Make the importer aware of the selection policy.  Change the import to an ImportMany (and the corresponding property to a collection), and have the importer choose which export to use from the list.  You can avoid duplicating the code to select the service to use in each importer by writing a custom collection class (which implements ICollection<T>), with an accessor for getting the desired T.  Then you can use [ImportMany(typeof(IService))] PolicyBased<IService> (where PolicyBased<T> is the custom collection you wrote) and let the reusable prioritization mechanism do the work for you. Many different schemes along these lines can co-exist.

b) Use two different contracts. In this instance the importer can go ahead and write [Import] IService and pretend things are simple. An exporter, on the other hand would need to [Export("my.namespace.PrioritizedService")] and another part would import all of the prioritized services, pick one, and export it under the IService type-derived contract.

It's worth keeping in mind that in both cases the selection mechanism is required to produce something meaningful, so you'd have to come up with a default IService implementation.

c) If you really must make all of this transparent per your original request, yes that can also be achieved. Just keep in mind that you're opting out of widespread reuse of the functionality you write that depends on it. What you need to do is define your own custom topology of ExportProviders for a container. Implementing a full-fledged ExportProvider can be tricky, but implementing one as a filter over other full-fledged implementations is relatively straightforward. Our own AggregateExportProvider uses a prioritization scheme of its own along these lines (exporters found earlier in its list are preferred over ones found later for the purpose of satisfying imports of exactly one implementation.) For more information on how to do this, see Glenn Block’s blog post on customizing container behavior with defaults.