MEF: Dependencies are Queries?

Start to schematically represent any component system and you’re likely to come up with a diagram like:

image

So what do the dependency and the service actually mean?

In most implementations, each is associated with a key:

image

Here the Screen Renderer component needs the Typesetter service, which the Fast Typesetter component can provide.

Dependency resolution is a done by matching keys – resolving the Screen Renderer’s dependency boils down to doing a dictionary lookup with Typesetter as the key, to find the Fast Typesetter implementation.

This is the case whether the dependencies are expressed as strings, types, or some combination of the two.

While this model is very powerful and can be taken a long way, ‘open’ systems can benefit from an alternative approach.

Imagine a system in which multiple renderers and typesetters exist, each with distinct capabilities:

image

The dependencies above are no longer evaluated using simple equality comparisons.

On the right, the capabilities of each component are represented as structured data: the service provided, plus a collection of key-value pairs bearing more detailed information. Together these are a service definition.

On the left, the dependencies of each component are represented as predicates over the possible service definitions. The predicate evaluates to true if the service definition can satisfy the dependencies.

So, the Print Renderer’s requirements, { Quality == High } , are true when evaluated against the Accurate Typesetter’s service definition, and false when executed against the Fast Typesetter’s.

In effect, components express their dependencies as queries over the entire set of available services.

To get an idea of where the power of this technique comes from, consider the way the example will compose: the Screen Renderer gets a fast Typesetter and the Print Renderer gets an accurate one. In a system with a better Typesetter – one that is both fast and accurate – a single Typesetter implementation could fulfill both dependencies.

Constraints aren’t just limited to equality comparisons either – if speed is expressed in characters per second, the Screen Renderer could specify { Speed > 500 } .

If you haven’t guessed already, this is the model that the MEF Primitives use. The service definitions are exposed with the type ExportDefinition. The predicate for testing ExportDefinitions for suitability is encoded in ImportDefinition.Constraint.

image

One of the constraints in the example might be expressed as:

image 

The system is elegant, but there are a few things to be aware of. The first that comes to mind is how the constraint is Boolean, so ‘falling back’ to a slow Typesetter when no fast one is available means expressing two separate constraints

Because ImportDefinition.Constraint is a Linq expression, and thanks to some special-case logic, MEF is able to resolve many dependencies without doing the linear scan that the query implies. This is only the case when constraints take the typical forms expressible in MEF’s Attributed Programming Model. Arbitrary constraints can result in a linear scan (although the way is clear to optimizing these in the future.)

One other interesting note – it is technically possible to transform an import constraint into a format that could be evaluated by a database-backed Linq provider… If for some reason you’d like to do that :)

You might be wondering why code snippets like the predicate above don’t appear in the MEF examples you typically see. The answer is that this is the key to understanding programming models. A programming model, in the MEF sense, translates some easy-to-type format like the Import and Export attributes, into constraints under-the-hood.

Constraint-based dependency resolution is one of the foundations that future versions of MEF will build upon.