代码改变世界

Modular ASP.NET MVC using the Managed Extensibility Framework (MEF), Part Three

2013-02-03 21:34  yezhi  阅读(477)  评论(0)    收藏  举报

http://www.fidelitydesign.net/?p=159

Firstly, sorry it’s been so long since my last MVC+MEF post, been a bit busy with life in general. Still haven’t managed to get my project onto CodePlex, but we shall see soon!

Ok, so where did we get to last time? We’ve managed to build a prototype MVC+MEF framework that supports modular areas and strongly-typed views. A couple of readers have been asking how we can incorporate IoC into our framework, and the question is really: do we need to?

This framework was originally conceived as a starting point for building a web application, so we could essentially allow MEF do handle our DI for us. But its not often the case that we can simply start a new project, but build off an existing project, and to go one stage further, if you have a lot of your existing code infrastructure built around an existing IoC container, then you really need to expose that existing infrastructure to your website.

The good news is there is a way, and it’s rather easy. Glenn Block (everyone’s favourite MEF guy) pointed me to a rather interesting project, called the Common Services Locator, you can find it on CodePlex here.

The CSL project is geared to providing a common abstraction over service location (i.e. accessing and instantiating types, regardless of which third-party IoC container you are using). As long as they provide a CSL provider, we can leverage them relatively easily with out MEF+MVC framework.

So this blog post is essentially going to cover the integration of an IoC container (actually, 2, just to demonstrate how easy it is) into our framework. But before we can do that, we really need to look at refactoring bits of our code. Let’s begin with composition.

Refactoring Composition

In the previous version of the framework, we had all composition taking place within our Application class. This locks both the CompositionContainer, and its associated Catalogs and ExportProviders, hiding them away from the public. If you want to leverage composition in another part of your site, it was exactly easy. So, in steps our new Composer class. This essentially is a wrapper around all the composition functionality of the project, and is something we can easily pass round. Let’s have a look:

/// <summary>
/// Provides a common mechanism for composing parts.
/// </summary>
publicclassComposer:IComposer
{
 
//
}

The Composer wraps up the management of the container, as well as adding any catalogs and export providers. This allows us to easily re-build the catalog as and when we need. When we want to composer a singular instance, we can easily do:

/// <summary>
/// Composes the specified object.
/// </summary>
/// <param name="object">The object to be composed.</param>
publicvoidCompose(object@object)
{
   
if(@object==null)
       
thrownewArgumentNullException("object");

   
if(Catalog==null)
       
return;

   
EnsureContainer();
   
Container.ComposeParts(@object);
}

/// <summary>
/// Ensures the Container has been instantiated/re-instantiated if the Composer has been modified.
/// </summary>
privatevoidEnsureContainer()
{
   
if(modified ||Container==null)
   
{
       
if(Container!=null)
           
Container.Dispose();

       
Container=newCompositionContainer(Catalog,ExportProviders.ToArray());

       
foreach(var provider in postContainerModifiers.Keys)
            postContainerModifiers
[provider](provider,Container);

       
if(ExportSelf)
       
{
           
var batch =newCompositionBatch();
            batch
.AddExportedValue<IComposer>(this);
           
Container.Compose(batch);
       
}

        modified
=false;
   
}
}

I’ve added an interface, IComposer, which provides a contract by which we can export the Composer itself. This may be useful in scenarios where you want to resolve type instances manually witout worrying about MEF itself. Because of this, the terminology I’ve used for the interface is much more akin to an IoC container, so methods like Resolve and ResolveAll are exposed here. You could then import/inject an instance of the composer if you wanted to:

[ImportingConstructor]
publicMyType(IComposer composer){
 
var instance = composer.Resolve<IMyMagicType>();
}

Although really, if you know what types your going to be using, you could just inject them directly instead of using the composer.

We’ve had to refactor the Application class itself to now support the Composer, luckily this wasn’t a lot of work:

#region Properties
/// <summary>
/// Gets the <see cref="Composer" /> used to compose parts.
/// </summary>
publicstaticComposerComposer{get;privateset;}
#endregion

#region Methods
/// <summary>
/// The start method of the application.
/// </summary>
protectedvoidApplication_Start()
{
   
// Perform any tasks required before composition.
   
PreCompose();

   
// Create the composer used for composition.
   
Composer=CreateComposer();

   
// Compose the application.
   
Compose();

   
// Set the controller factory.
   
ControllerBuilder.Current.SetControllerFactory(ControllerFactory);

   
// Set the view engine that supports imported areas.
   
ViewEngines.Engines.Add(newAreaViewEngine());

   
// Initialises the application.
   
Initialise();

   
// Register MVC routes.
   
RegisterRoutes();
}

/// <summary>
/// Creates a <see cref="Composer" /> used to compose parts.
/// </summary>
/// <returns></returns>
protectedvirtualComposerCreateComposer()
{
   
var composer =newComposer();

   
GetDirectoryCatalogs()
       
.ForEach(composer.AddCatalog);

    composer
.AddExportProvider(
       
newDynamicInstantiationExportProvider(),
       
(provider, container)=>((DynamicInstantiationExportProvider)provider).SourceProvider= container);

   
return composer;
}

We once again make the CreateComposer method virtual, so if you do want exact control of how the Composer is created, possibly to wire up additional export providers/catalogs etc, you can override this and do what you want.

Refactoring Exports

Currently we manage our exports through a combination of the ExportAttribute and the ExportMetadataAttribute types. This is great, its easy, but does lead to problems, particularly when metadata is missing this is required for composition. This is important, as MEF will not compose types which do not satisfy the required import definitions. Consider our Controller type, currently we see this:

[Export(typeof(IController)),ExportMetadata("Name","Blog")]
publicBlogController:Controller
{

}

If we forget the ExportMetadataAttribute attribute, the controller will not be composed. What we really need to do, is ensure that the controller metadata is passed with the export. We can create a custom export attribute which provides this metadata:

/// <summary>
/// Exports a controller.
/// </summary>
[MetadataAttribute,AttributeUsage(AttributeTargets.Class,AllowMultiple=false)]
publicclassExportControllerAttribute:ExportAttribute,IControllerMetadata
{
   
#region Constructor
   
/// <summary>
   
/// Initialises a new instance of the <see cref="ExportControllerAttribute"/> class.
   
/// </summary>
   
/// <param name="name">The name of the controller.</param>
   
publicExportControllerAttribute(string name):base(typeof(IController))
   
{
       
if(string.IsNullOrEmpty(name))
           
thrownewArgumentException("Controller name cannot be null or empty.");

       
this.Name= name;
   
}
   
#endregion

   
#region Properties
   
/// <summary>
   
/// Gets the name of the controller.
   
/// </summary>
   
publicstringName{get;privateset;}
   
#endregion
}

This export attribute ensures that we have the controller name alongside the export. One thing to note, is you don’t actually have to decorate this type with your metadata interface, MEF will project the required properties for you, but I like to do so, as I know it ensures that my export attribute is conforming to my required metadata interface.

Now, we can simply our export:

[ExportController("Blog")]
publicBlogController:Controller
{

}

Integrating Unity into MEF+MVC

Ok, finally onto what we really want to do, IoC integration :) . To add support for Unity (and any compatible CSL provider, I’ve added a bit of code supplied by Mr. Block, the CSLExportProvider (read: Glenn’s code). We can augment this ExportProvider into our Composer, and then wire up our Unity container:

publicclassMvcApplication:Application
{
   
#region Fields
   
privateIUnityContainer unityContainer;
   
privateCSLExportProvider exportProvider;
   
#endregion

   
#region Methods
   
/// <summary>
   
/// Creates the instance of the Unity container.
   
/// </summary>
   
protectedoverridevoidPreCompose()
   
{
        unityContainer
=newUnityContainer();

       
var locator =newUnityServiceLocator(unityContainer);
        exportProvider
=newCSLExportProvider(locator);

        unityContainer
.AddExtension(newCSLExportProviderExtension(exportProvider));

       
RegisterTypes();
   
}

   
/// <summary>
   
/// Registers any required types for the Unity container.
   
/// </summary>
   
protectedvoidRegisterTypes()
   
{
        unityContainer
.RegisterType<ITicketSystem,SimpleTicketSystem>();
   
}

   
/// <summary>
   
/// Creates the composer used for composition.
   
/// </summary>
   
/// <returns></returns>
   
protectedoverrideMefMvcFramework.Composition.ComposerCreateComposer()
   
{
       
var composer =base.CreateComposer();
        composer
.AddExportProvider(exportProvider);

       
return composer;
   
}
   
#endregion
}

In the example above, we are taking advantage of our Application class design to control how the Composer is constructed in relation to our IoC Container, Unity. We are then registering a simple example type, the ITicketSystem. Because we’ve registered this type with Unity, thanks to the Common Services Locator provider, we can now access these when importing/injecting into our target types. Let’s create a new MVC Controller to demonstrate this:

[ExportController("Support")]
publicclassSupportController:Controller
{
   
#region Fields
   
privateITicketSystem ticketSystem;
   
#endregion

   
#region Constructor
   
[ImportingConstructor]
   
publicSupportController(ITicketSystem ticketSystem)
   
{
       
this.ticketSystem = ticketSystem;
   
}
   
#endregion

   
#region Actions
   
publicActionResultIndex()
   
{
       
var ticket = ticketSystem.CreateTicket();
       
returnContent(ticket.Title);
   
}
   
#endregion
}

With our new controller, when we create an instance, we inject our ITicketSystem instance through the constructor. The important thing here, is that our instance of ITicketSystem is provided by our IoC container (currently Unity), exposed via MEF. Thus demonstrates just how flexible and robust MEF really is.

We can even do it for another IoC container, let’s try Autofac:

publicclassMvcApplication:Application
{
 
#region Fields
 
privateCSLExportProvider exportProvider;
 
#endregion

 
#region Methods
 
/// <summary>
 
/// Creates the instance of the Unity container.
 
/// </summary>
 
protectedoverridevoidPreCompose()
 
{
     
var container =newContainerBuilder().Build();
      exportProvider
=newCSLExportProvider(newAutofacServiceLocator(container));

     
RegisterTypes(container);
 
}

 
/// <summary>
 
/// Registers any types required for the container.
 
/// </summary>
 
/// <param name="container">The container.</param>
 
privatevoidRegisterTypes(IContainer container)
 
{
     
var builder =newContainerBuilder();

      builder
.RegisterType<SimpleTicketSystem>().As<ITicketSystem>();
      exportProvider
.RegisterType(typeof(ITicketSystem));

      builder
.Update(container);
 
}

 
/// <summary>
 
/// Creates the composer used for composition.
 
/// </summary>
 
/// <returns></returns>
 
protectedoverrideMefMvcFramework.Composition.ComposerCreateComposer()
 
{
     
var composer =base.CreateComposer();
      composer
.AddExportProvider(exportProvider);

     
return composer;
 
}
 
#endregion
}

Minimal code change makes me very happy! Any CSL compatible provider can be used, allowing enormous flexibility and integration options. Hope you like it!

Download VS2010 Project

UPDATE For Martyn: wiring up Castle Windsor was really easy, in fact took my only a few minutes to wire it up and add a component to the container. This is again thanks to the CSL project, whereby you can grab a CSL-provider for Castle Windsor.

The code change is as follows:

namespaceMefMvcApplication
{
   
usingCastle.Windsor;
   
usingCommonServiceLocator.WindsorAdapter;

   
usingMefMvcFramework.Example.TicketSystem;
   
usingMefMvcFramework.ServiceLocation;
   
usingMefMvcFramework.Web;

   
publicclassMvcApplication:Application
   
{
       
#region Fields
       
privateCSLExportProvider exportProvider;
       
#endregion

       
#region Methods
       
/// <summary>
       
/// Creates the instance of the Unity container.
       
/// </summary>
       
protectedoverridevoidPreCompose()
       
{
           
var container =newWindsorContainer();
            exportProvider
=newCSLExportProvider(newWindsorServiceLocator(container));

           
RegisterTypes(container);
       
}

       
/// <summary>
       
/// Registers any types required for the container.
       
/// </summary>
       
/// <param name="container">The container.</param>
       
privatevoidRegisterTypes(IWindsorContainer container)
       
{
            container
.AddComponent("TicketSystem",typeof(ITicketSystem),typeof(SimpleTicketSystem));
            exportProvider
.RegisterType(typeof(ITicketSystem));
       
}

       
/// <summary>
       
/// Creates the composer used for composition.
       
/// </summary>
       
/// <returns></returns>
       
protectedoverrideMefMvcFramework.Composition.ComposerCreateComposer()
       
{
           
var composer =base.CreateComposer();
            composer
.AddExportProvider(exportProvider);

           
return composer;
       
}
       
#endregion
   
}
}

The Castle Windsor variant of the project is attached. Hope that helps.

Download VS 2010 Project (Castle Windsor)