Thursday, April 2, 2009

Unleashing modules – part 2

This is a continuation to the post that I did a couple of days ago about loading AutoFac-modules with the help of MEF (Managed Extensibility Framework). In this post I’ll show and discuss my implementation.

The implementation

If I have an assembly that has an AutoFac module that sets up the dependencies for that assembly I would like to “export” this module through the magic of MEF. Rather than exporting the modules themselves I chose to export a delegate type that I’ve named ModuleFactory. The signature is as follows:

using Autofac;

namespace Legend.Autofac
{
    /// <summary>
    /// Creates an <see cref="IModule" /> that's responsible for loading
    /// dependencies in the specified context.
    /// </summary>
    /// <param name="mode">The mode the application is running in, let's the implementor
    /// of the factory load different dependenices for different modes.</param>
    /// <returns>An <see cref="IModule" />.</returns>
    public delegate IModule ModuleFactory(ApplicationMode mode);
}

 

As you see this factory delegate type takes an argument called “mode” of the type ApplicationMode, this is an enum of different modes an application can be run in (test, staging and production) and gives the factory a chance to load different dependency configuration depending on the mode of the application. For example, if a library communicates with a web-service, in test mode you might want to use a fake version of this web-service. As you will see later this mode is set on the ComposedModule and it’s the module that passes it to the factories. I’ve been going back and forward on this one, thinking that I might want to use a string instead of an enum so that it’s the set of options is not closed but I’m leaning towards the enum-solution. This is the definition of the enum:

namespace Legend.Autofac
{
    /// <summary>
    /// Describes different modes an application can run in.
    /// </summary>
    public enum ApplicationMode
    {
        /// <summary>
        /// The application is running in a test environment.
        /// </summary>
        Test = 0,
        
        /// <summary>
        /// The application is running in a staging environment.
        /// </summary>
        Staging = 1,

        /// <summary>
        /// The application is running in production environment.
        /// </summary>
        Production = 2
    }
}

So, last but not least, the ComposedModule itself. As I mentioned earlier, the module takes in a ComposablePartCatalog in it’s constructor and uses this catalog to load all the exported ModuleFactories. I also provide an overload of the constructor that takes a string that’s a path to a directory containing dll’s that contains your exports, a MEF DirectoryCatalog will be created for this directory. The constructor also takes in the ApplicationMode that will be passed to the ModuleFactories.

using Autofac.Builder;
using Autofac;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Collections.Generic;
using System;

namespace Legend.Autofac
{
    /// <summary>
    /// An <see cref="Autofac.Builder.Module" /> is composed by with the help of
    /// the System.ComponentModel.Composition (MEF) framework.
    /// </summary>
    public class ComposedModule
        : global::Autofac.Builder.Module
    {
        #region Fields
        private ApplicationMode mode;
        private ComposablePartCatalog catalog;
        [Import(typeof(ModuleFactory))]
        private IEnumerable<ModuleFactory> RegisteredModuleFactories; 
        #endregion

        #region Construction
        /// <summary>
        /// Creates a new ComposedModule using the specified catalog to
        /// import ModuleFactory-instances.
        /// </summary>
        /// <param name="mode">The mode the application is running in.</param>
        /// <param name="catalog">The catalog used to import ModuleFactory-instances.</param>
        public ComposedModule(ApplicationMode mode, ComposablePartCatalog catalog)
        {
            if (catalog == null) throw new ArgumentNullException("catalog");

            this.mode = mode;
            this.catalog = catalog;

            this.ImportFactories();
        }

        /// <summary>
        /// Creates a new ComposedModule that loads all the ModuleFactories
        /// exported in assemblies that exists in the directory specified in 
        /// the <param name="modulesDirectoryPath" />-parameter.
        /// </summary>
        /// <param name="mode">The mode the application is running in.</param>
        /// <param name="catalog">The catalog used to import ModuleFactory-instances.</param>
        public ComposedModule(ApplicationMode mode, string modulesDirectoryPath)
            : this(mode, new DirectoryCatalog(modulesDirectoryPath)) { } 
        #endregion

        #region Methods
        private void ImportFactories()
        {
            var batch = new CompositionBatch();
            batch.AddPart(this);

            var container = new CompositionContainer(this.catalog);
            container.Compose(batch);
        }

        protected override void Load(ContainerBuilder builder)
        {
            base.Load(builder);

            foreach (var factory in this.RegisteredModuleFactories)
            {
                var module = factory(this.mode);
                builder.RegisterModule(module);
            }
        } 
        #endregion
    }
}

 

Using it

All you have to do to use it is to create modules and export ModuleFactory methods like this:

public class FooModule
    : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder
            .Register(x => new Foo())
            .As<IFoo>()
            .FactoryScoped();
    }

    [Export(typeof(ModuleFactory))]
    public static ModuleFactory Factory = x => new FooModule();
}

 

That’s all you need, and then of course register the ComposedModule in you container!

If you have any comments, suggestions, questions or so, please don’t hesitate to comment here or catch me over at Twitter: @patrik_hagne.

I’m going to publish the source in some place better soon, but for now grab it here.

3 comments:

  1. One thing that you should probably be aware of is that static exports won't be supported in the newer MEF versions.

    MEF's metadata features might be another option for determining which 'environment' a module should be used in.

    E.g. you can create a metadata attribute:

    [MetadataAttribute]
    public class ApplicationModeAttribute : Attribute
    {
    public ApplicationMode ApplicationMode { get; private set; }

    public ApplicationModeAttribute(ApplicationMode mode) {
    ApplicationMode = mode;
    }
    }

    This gets applied to modules that export IModule:

    [Export(typeof(IModule))]
    [ApplicationMode(ApplicationMode.Staging)]
    public class FooModule : IModule ..

    The mode can then be consumed via a Metadata View:

    public interface IModuleMetadata
    {
    [DefaultValue(ApplicationMode.Any)]
    ApplicationMode ApplicationMode { get; }
    }

    MEF will generate implementations of these for you:

    var modules = container.GetExports<IModule, IModuleMetadata>();
    foreach (var module in modules)
    if (module.MetadataView.ApplicationMode == currentMode)
    container.RegisterModule(module.GetExportedObject());

    No modules will be created until the GetExportedObject() call - nice and lazy :)

    Hope this is interesting.. The MEF metadata capabilities are really handy for this kind of conditional inclusion scenario.

    Great article!

    Nick

    ReplyDelete
  2. @Nick, thank you very much for your comments, I'll go look into the metadata-features of MEF this week. I'd be interested to know the reasoning behind removing static exports, I don't doubt it's a well thought through decision, I just fail to see the benefit.

    ReplyDelete
  3. Hi Patrik,

    The main problem with static exports is that they're not consistent with the semantics of the rest of the attributed programming model.

    For example, there's no way to adjust creation policy (factory/singleton) or to participate in component lifecycle (INotifyImportSatisfaction.)

    Given that there's a simple workaround (create a non-static class that exports the static member from the other class through a property) the inconsistency and potential confusion of static imports doesn't seem worthwhile.

    Might have been other issues that I've forgotten - it was a while ago that the team discussed this.

    Cheers,
    Nick

    ReplyDelete