Udostępnij za pośrednictwem


Overriding part registration conventions with the MEF attributes [Nick]

This post discusses features in the preview version of MEF, and some details may change between now and the full release.

One of the big advances made in the new MEF preview is a convention-driven programming model aimed at eliminating repetitive attribute usage.

In this new model, the MEF attributes like Export and Import have a new role as the clean and powerful mechanism for overriding the registration conventions.

Configuration by exception

The power in convention-driven rules is that they require 'constant effort' to handle an arbitrary number of parts.

     var registrationBuilder = new RegistrationBuilder();

    registrationBuilder.ForTypesDerivedFrom<IMessageHandler>()
        .Export<IMessageHandler>()
        .SetCreationPolicy(CreationPolicy.NonShared);

The rule above remains unchanged whether there are 10 or 10,000 message handlers in an application, while using explicit configuration for each hander would result in extra effort expended for every handler added.

Inevitably, there are edge cases and parts for which a rule doesn’t quite fit. How these cases are handled has an impact on the effectiveness and maintainability of a convention-based system.

Handling non-conforming parts (“exceptions”) via rules

It is possible to embed exceptions to part discovery/configuration rules into the rules themselves. This approach does not scale well to many parts or parts distributed between loosely-coupled assemblies.

In an application with 50 message handler parts, where 10% have some need of special handling, conventions in MEF's syntax would look like:

     registrationBuilder.ForTypesMatching(t =>
           t != typeof(OrderPlacedMessageHandler) &&
           t != typeof(NewCustomerMessageHandler) &&
           t != typeof(CheckoutMessageHandler) &&
           t != typeof(CheckBouncedMessageHandler) &&
           t != typeof(OutOfStockMessageHandler) &&
           typeof(IMessageHandler).IsAssignableFrom(t))
        .Export<IMessageHandler>()
        .SetCreationPolicy(CreationPolicy.NonShared);

    // Special contract name
    registrationBuilder.ForType<OrderPlacedMessageHandler>()
        .Export<IMessageHandler>(x => x.Named("orders"))
        .SetCreationPolicy(CreationPolicy.NonShared);

    // Default creation policy
    registrationBuilder.ForType<NewCustomerMessageHandler>()
        .Export<IMessageHandler>();

    // Similar single-component rules for remaining three parts above.
    // ...

This has the following issues:

  1. Applying an exception requires tinkering with the host, which is more complicated than changing an individual part
  2. As soon as there is even one exception, the declarative 'syntactic sugar' of ForTypesDerivedFrom() becomes inapplicable
  3. All exceptional types must be visible from the code defining the rule - this does not work well with dynamic discovery e.g. DirectoryCatalog
  4. The rule becomes unreadable and verbose
  5. Keeping the 'shared' aspects of the conventions in sync between the single-component registrations and the general case may introduce errors
  6. The rule text may be a point of source control contention on large product teams

Handling exceptions via attributes

Fortunately, MEF has deep support for attributed configuration, which provides a way to specify exceptions to the rules without the issues discussed above.

In a MEF application, the rule can remain unchanged.

     registrationBuilder.ForTypesDerivedFrom<IMessageHandler>()
        .Export<IMessageHandler>()
        .SetCreationPolicy(CreationPolicy.NonShared);

Then, for example OrderPlacedMessageHandler can have its particular contract name expressed by the addition of an export attribute.

     [Export("orders", typeof(IMessageHandler))]
    class OrderPlacedMessageHandler : IMessageHandler {
    }

The NewCustomerMessageHandler can modify its creation policy.

     [PartCreationPolicy(CreationPolicy.NonShared)]
    class NewCustomerMessageHandler : IMessageHandler {
    }

In contrast with the rule-based approach preceding it, the attributed approach has the following benefits:

  1. A developer applying an exception only needs to deal with the aspect of the part they want to configure, rather than unrelated hosting code
  2. The rule does not change in the presence of exceptions
  3. There is no duplication of the parts of the rule that are not affected by the exception
  4. The rule is not coupled to the implementation types
  5. Developers viewing the 'exceptional' part source code can see that it is treated differently, for example, avoiding threading bugs caused by variations in the sharing model

How attributes and rules interact

Where a rule and an applied attribute overlap, the attribute will always take precendence over the rule.

MEF attributes in a part’s source file always override any rules that affect the same aspect of the same member.

The groups of attributes that are considered ‘aspects’ of a member are:

  • Export - the Export attribute, ExportMetadata, InheritedExport, custom export and metadata attributes
  • Import - the Import and ImportMany attributes
  • Constructor selection - the ImportingConstructor attribute
  • Part creation policy - the PartCreationPolicy attribute
  • Part metadata - the PartMetadata attribute

The source code items considered as a single member for the purpose of convention overrides are:

  • The class declaration
  • All constructors, considered as one group
  • A single constructor parameter within a constructor
  • A single property
  • A single method

Examples

Let’s take a look at some common situations where attributes may be used to override conventions.

Implementing decorators by changing the exported contract name

In the Decorator Design Pattern, one part 'wraps' another part, providing the same contract but adding funtionality to what the wrapped part provides.

For this scenario, an example convention specifies that public interfaces on a certain set of types are exported:

     registrationBuilder.ForTypesMatching(t => t.Namespace.EndsWith(".Parts"))
        .ExportInterfaces(i => i.IsPublic);

The RemoteProductRepository part provides the IProductRepository contract, while a second part, ProductCache, is introduced in order to improve product retrieval performance.

These are hooked up in a chain, so that other parts importing IProductRepository will be given the cache, while the cache itself imports the underlying repository.

The convention that applies to these types is that they export their implemented interfaces.

     public interface IProductRepository {
        Product GetById(int id);
    }

    [Export("impl", typeof(IProductRepository))]
    class RemoteProductRepository : IProductRepository { ... }

    class ProductCache : IProductRepository {
        public ProductCache([Import("impl")] IProductRepository impl) { ... }
    }

The named export and import ensure that only the ProductCache sees the decorated repository.

Changing export metadata to customise behavior

The TaskRunner component imports task components that it runs periodically. Metadata is used to determine the frequency at which the task runs.

     public interface ITask {
        void Run();
    }

    class TaskRunner : ITaskRunner {
        public TaskRunner(IEnumerable<Lazy<ITask,ITaskFrequency>> tasks) { ... }
    }

Most tasks run hourly, so this is the default by convention.

     registrationBuilder.ForTypesDerivedFrom<ITask>()
        .Export<ITask>(x => x.AddMetadata(“Frequency”, 60));

The SendMailTask runs every 5 minutes and uses an override to specify this.

      [Export(typeof(ITask)), ExportMetadata("Frequency", 5)]
    class SendMailTask : ITask { ... }

Selecting a shorter constructor

By default a part configured with RegistrationBuilder will use the constructor with the most parameters. In this example, the longer constructor is for unit testing purposes only, so the shorter constructor is marked with an attribute.

     class MyPart {
        public MyPart(IFoo foo, IBarTestStub bar) { }

        [ImportingConstructor]
        public MyPart(IFoo foo) { }
    }

Changing the contract name of an import for explicit wiring

If a property is selected as an import by convention, an Import attribute can be applied to the property to override the contract name or other facet of the ImportDefinition. This is useful to implement explicit wiring.

     class MyPart {
        [Import("impl")]
        public IFoo Foo { get; set; }
    }

    class Foo1 : IFoo { }

    [Export("impl", typeof(IFoo))]
    class Foo2 : IFoo { }

Here the export and import attributes specify names so that the Foo import on MyPart will be provided with a Foo2 instance.

Opting out

If a type that would otherwise be matched by a rule should not be used as a part, it can be marked with the PartNotDiscoverable attribute:

     [PartNotDiscoverable]
    class MyPart {… }

Diagnostics

Whenever a rule is overridden by a source-level attribute, you can find a diagnostic message in the debugger trace output. This helps keep track of where the exceptions to the rules are. The message looks like:

     A convention that would apply to member A of type B has been overridden
    by attributes applied in the source file.

Conclusions

When using conventions to register parts in MEF, rules can be kept clean and simple by using the standard MEF attributes to override rules where necessary.

We hope you enjoy working with the new preview; we deeply value your feedback and would love to hear about your experiences via the Discussion Forum or Issue Tracker on the MEF developer preview site.

Coming up on this blog we’ll explore the other major addition to MEF in version 2 – composition scoping.

Download MEF 2 Preview 4 from the CodePlex site.