Getting started with convention-based part registration in MEF 2 [Nick]

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

MEF ‘version 1’ uses attributes to describe how a type should be handled for composition purposes.

     [Export, PartCreationPolicy(CreationPolicy.NonShared)]
    public class HomeController :  Controller
    {
        [ImportingConstructor]
        public HomeController(/* Dependencies here */) { … }
        public ActionResult Index() { … }
    }

Attributes are a good fit for plug-in development when building extensible applications, and we haven't changed any of this experience in the new version. Attributes are still a great way to unambiguously mark-up types for composition.

Planning MEF ‘version 2’ we observed that in large MEF-based applications, only a small proportion of imports and exports are actually extensibility related. A large amount of composition happens to support the internal architecture of the application itself. In many cases, applications that use MEF don’t provide a third-party plug-in model at all, and use MEF purely as a way to bring the benefits of loosely-coupled architecture to monolithic apps.

In the context of a single application, it is much easier to set and communicate rules about how parts should be written, loosening up the need to explicitly specify every detail of every part.

In the example above, if all controllers are exported, all have the NonShared creation policy, and all use a single constructor to receive dependencies, then it is in some ways harmful to repeat that information using attributes on every single controller class. Given these ‘norms’ for a controller, a programmer working in a structured application should be able to write:

     public class HomeController :  Controller
    {
        public HomeController(/* Dependencies here */) { … }
        public ActionResult Index() { … }
    }

…and have the composition aspects inferred automatically. Less repetition leads to more readable code, less overall lines of code and fewer opportunities to make mistakes.

RegistrationBuilder is a new MEF feature coming in .NET 4.5 that provides exactly this kind of experience. Using RegistrationBuilder, the rules defining controller parts look like:

     var conventions = new RegistrationBuilder();
    conventions.ForTypesDerivedFrom<Controller>()
        .Export()
        .SetCreationPolicy(CreationPolicy.NonShared);

Notice that the rule doesn’t say anything about how to find types that derive from Controller. Catalogs still play the role of locating types, while RegistrationBuilder rules replace the attribute syntax. So, to apply these rules to all types in an assembly, an AssemblyCatalog is used:

     Assembly controllersAssembly = // …
    var catalog = new AssemblyCatalog(controllersAssembly, conventions);
    var container = new CompositionContainer(catalog);

Notice the additional constructor parameter accepted by the catalog. All of the familiar MEF catalog types have been updated in the new version to support RegistrationBuilder conventions.

Building Conventions

Using RegistrationBuilder has two aspects:

  1. Identifying types that should be considered parts; and,
  2. Configuring types for use as parts.

Identifying Parts

Each rule is defined by calling one of the three For*() methods on RegistrationBuilder to specify which types the rule will apply to.

  • ForType<T>() — selects the single type T. For example ForType<HomeController>() would set up a rule for the concrete HomeController type.
  • ForTypesDerivedFrom<T>() — selects types assignable, but not equal to, a contract type T. This may be a base class or an interface. The example rule for identifying controllers earlier in this article uses this mechanism.
  • ForTypesMatching(Predicate<Type> predicate) — selects types that match a Boolean selector. For example, ForTypesMatching(t => t.Name.EndsWith("Repository")) will select all types whose name ends with the word “Repository”. This is the most flexible option.

Multiple rules may match the same type. In such cases, as long as the rules don’t overlap in an incompatible way, all matching rules are applied.

The selector methods above return a PartBuilder that can then be used to configure the matching types for use as parts.

Configuring Parts

Rules can be used to configure all aspects of a MEF part:

  • Exported interfaces and properties, with metadata
  • Importing constructors, constructor parameters and properties
  • Part creation policy
  • Part metadata

Exports

Exports can be configured at the type declaration/interface level, or via properties in the case of property exports.

At the level of the part type itself, the concrete type of the part can be exported (in this case MainWindow):

     conventions.ForType<MainWindow>()
        .Export()

Or, more commonly, one or more of its interfaces will be exported:

     conventions.ForType<MainWindow>()
        .Export<IView>();

Multiple interfaces can be exported using a filter:

     conventions.ForType<MainWindow>()
        .ExportInterfaces(i => i.IsPublic);

Overloads exist for configuring the export with a contract name or with metadata:

     conventions.ForType<MainWindow>()
        .Export<IView>(x => x.AddMetadata("Name", "Main"))

Properties can be exported using a similar syntax. If the concrete type is specified, properties can be selected using a Linq expression:

     conventions.ForType<MainWindow>()
        .ExportProperty(mw => mw.Notifier);

When the concrete type is not known, for example with ForTypesMatching() , properties can be exported by selecting them:

     conventions.ForTypesMatching(t => t.Namespace == "MyApp.Windows")
        .ExportProperties(pi => pi.IsPublic);

Multiple export-related configuration rules on the same part are additive, so a type can export several contracts:

     conventions.ForType<MainWindow>()
        .Export<IView>()
        .Export<ICloseable>();

Imports

Imports are specified either at the constructor or at the property level.

By default, the RegistrationBuilder will select the public constructor with the most parameters as the importing constructor.

To select a different constructor, the SelectConstructor() method is used.

     conventions.ForTypesDerivedFrom<IView>()
        .SelectConstructor(ctors =>
            ctors.Min(ctor => ctor.GetParameters().Length));

When the concrete type of the part is known at configuration time, the constructor can be selected using strongly-typed syntax:

     conventions.ForType<MainWindow>()
        .SelectConstructor(pb => new MainWindow(pb.Import<ILogger>()));

By default, any constructor parameters or imported properties of an enumerable or collection type will be considered to “Import Many” .

The syntax for performing property imports mirrors the syntax described for property exports previously.

Part Creation Policy

By default, parts configured by RegistrationBuilder will use the standard MEF CreationPolicy.Any setting. In normal use, this will boil down to ‘single instance per container’ sharing, unless the part is created using an ExportFactory<T> .

To select the creation policy, use the SetCreationPolicy() method:

     conventions.ForTypesDerivedFrom<Controller>()
        .SetCreationPolicy(CreationPolicy.NonShared);

(Note, the ASP.NET MVC integration also included in the preview interprets CreationPolicy.Any and CreationPolicy.Shared to mean ‘single instance per HTTP request.’ To make a part use ‘single instance per application’ sharing under ASP.NET MVC, you need to apply the ApplicationShared attribute to the part.)

Part Metadata

Part metadata is useful when filtering parts in customized catalogs, or when setting up a CompositionScopeDefinition. Part metadata can be added explicitly, as in:

     conventions.ForType<OutputWindow>()
        .AddMetadata("Mode", "Debug");

Or, it can be calculated from the type:

     conventions.ForType<OutputWindow>()
        .AddMetadata("Mode", t => GetMode(t));

Overriding Rules

Inevitably, there are exceptions to every rule. In these situations, the MEF attributes can often be used to override the rules on a case-by-case basis. This is the topic of an upcoming post – stay tuned!

Download MEF 2 Preview 4 from the CodePlex site.