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.

Comments

  • Anonymous
    April 15, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    April 16, 2009
    Doesn't this make service consumers more tightly coupled with service providers?  If I need a fast typesetter why wouldn't I resolve an IFastTypeSetter?  Or add RenderFast and RenderAccurate methods to ITypeSetter? I think more concrete examples would aid the discussion.

  • Anonymous
    April 16, 2009
    Hi Bill! I'm not sure there is any more coupling - just a tighter contract. Examples that fit on a page are hard to come by... Compilers in an IDE may be another illustrative example. ICompiler is annotated with metadata describing the language that can be compiled. A build tool will want a compiler for a specific language, while a configuration tool may wish to list all of the available compilers. This is one of a set of scenarios that the techniques you described are harder to adapt to - using contracts with metadata is more flexible than creating interfaces for every specialisation, or methods for every combination of capabilities. Hope this illustrates the difference a little better - please keep the questions coming. Nick

  • Anonymous
    June 08, 2009
    With the Managed Extensibility Framework (MEF), you can use Import and Export attributes to declare what

  • Anonymous
    June 26, 2009
    Does this work today in MEF Preview 5?  I mean, is it possible to provide this sort of constraint?  I'm trying desperately to filter a list of exports that satisfy an import based on a piece of metadata but can't seem to get it to work.

  • Anonymous
    June 26, 2009
    The comment has been removed

  • Anonymous
    June 27, 2009
    The comment has been removed