Udostępnij za pośrednictwem


MEF and ASP.NET MVC sample

Disclaimer

First things first, don’t consider this sample an official sample. It is just something I’ve created to illustrate some capabilities of MEF when integrated with ASP.NET MVC in the user-level (as opposed to framework level).

Also, if you would try to do this integration yourself, you’ll notice at some point that you’ll need to attach metadata to your controllers, so you can grab the right one on the controller factory. In this sample I provide a custom catalog that is aware of controllers, so it adds the metadata itself saving us the burden. Of course, this adds to complexity..

As MEF targets the extensibility story, this sample is an ASP.NET MVC app that can be “augmented” by new modules. The main app is a shell with almost no functionality. New modules add navigation, controllers, services and consume the services are present on the system.

The first extension module provide adds Wiki support to the main application. The second, a forum module.

Bear in mind that everything is fake. I’m showing how to leverage the extensibilities capabilities of MEF, and not how to design a CMS, Forum or Wiki :-)

In a real world app a similar extensibility mechanism in a web app would also have to deal with database/versioning issues, content resource (additional css, js, images), and so forth.

Download

MvcWithMEF.zip

Walkthrough

You may start by running the app. You should see the following on your browser:

scl1

The links above (forum, wiki) were added dynamically by modules loaded.

Where everything starts

The Global.asax.cs holds our bootstrap code:

    1:  protected void Application_Start()
    2:  {
    3:    HostingEnvironment.RegisterVirtualPathProvider(
    4:      new AssemblyResourceVirtualPathProvider());
    5:   
    6:    RegisterRoutes(RouteTable.Routes);
    7:   
    8:    var catalog = new CatalogBuilder().
    9:      ForAssembly(typeof(IModuleInstaller).Assembly).
   10:      ForMvcAssembly(Assembly.GetExecutingAssembly()).
   11:      ForMvcAssembliesInDirectory(HttpRuntime.BinDirectory, "*Extension.dll"). 
   12:      Build();
   13:   
   14:    _container = new CompositionContainer(catalog);
   15:   
   16:    var modules = _container.GetExportedObjects<IModuleInstaller>();
   17:    var navService = _container.GetExportedObject<NavigationService>();
   18:   
   19:    InitializeModules(modules, navService);
   20:   
   21:    ControllerBuilder.Current.SetControllerFactory(
   22:      new MefControllerFactory(_container));
   23:  }

The first line sets up a custom VirtualPathProvider that check assembly resources for content. Nothing new.

Lines 8-12 defines our catalog, which encapsulates the discovery mechanism on MEF. It also provides us opportunities to customize things. In this case, we want to discover things on the Core assembly (line 9), on the executing assembly/web project assembly (line 10), and anything on the web app’s bin folder that ends with Extension.dll (line 11).

A container is created with a reference to the aggregation of all these catalogs. Right after we retrieve all IModuleInstaller implementations available and start all modules, which is to say we give them a chance to change/add URL routes and add navigation information.

Finally, we create a custom IControllerFactory instance and set up ASP.NET MVC to use it (line 21).

The MEF Controller Factory
    1:  public class MefControllerFactory : IControllerFactory
    2:  {
    3:    private const string ControllerExportEntryName = "controllerExport";
    4:    private readonly CompositionContainer _container;
    5:   
    6:    public MefControllerFactory(CompositionContainer container)
    7:    {
    8:      _container = container;
    9:    }
   10:   
   11:    public IController CreateController(RequestContext requestContext, string controllerName)
   12:    {
   13:      var controllerExport = _container.GetExports<IController>().
   14:        Where(exp => 
   15:          exp.Metadata.ContainsKey(Constants.ControllerNameMetadataName) &&
   16:          exp.Metadata[Constants.ControllerNameMetadataName].
   17:            ToString().ToLowerInvariant().
   18:            Equals(controllerName.ToLowerInvariant())).
   19:          FirstOrDefault();
   20:   
   21:      if (controllerExport == null)
   22:      {
   23:        throw new HttpException(404, "Not found");
   24:      }
   25:   
   26:      requestContext.HttpContext.Items[ControllerExportEntryName] = controllerExport;
   27:   
   28:      return controllerExport.GetExportedObject();
   29:    }
   30:   
   31:    public void ReleaseController(IController controller)
   32:    {
   33:      var export = HttpContext.Current.Items[ControllerExportEntryName] as Export<IController>;
   34:   
   35:      if (export != null)
   36:      {
   37:        _container.ReleaseExport(export);
   38:      }
   39:    }
   40:  }

This implementation, albeit short, shows interests aspects. First, on the CreateController, we get all exports of IController – which is different from getting all _instances_ of IController. Then, we use the metadata associated with those Exports to find the controller we need to get an instance.

We hold this export instance for the duration of this request (HttpContext.Items), so we can use it on ReleaseController. This relies on the assumption that in a request only one controller is instantiated.

The ReleaseController get back that export instance and calls Container.ReleaseExport, which will go over all the object graph, and properly release and dispose NonShared / Disposable instances. If you miss this step you will be in a route to a memory bloat as every controller is non shared and disposable.

The MVC Catalog

This one is a bit more complex, but remember, it is an optional step. I just added it because I’m lazy and didn’t want to add metadata to every controller (it would be a violation of DRY too.)

 public class MvcCatalog : ComposablePartCatalog, ICompositionElement
 {
   private readonly Type[] _types;
   private readonly object _locker = new object();
   private IQueryable<ComposablePartDefinition> _parts;
  
   public MvcCatalog(params Type[] types)
   {
     _types = types;
   }
  
   // More constructors //
  
   public override IQueryable<ComposablePartDefinition> Parts
   {
     get { return InternalParts; }
   }
  
   internal IQueryable<ComposablePartDefinition> InternalParts
   {
     get
     {
       if (_parts == null)
       {
         lock(_locker)
         {
           if (_parts == null)
           {
             var partsCollection = 
               new List<ComposablePartDefinition>();
  
             foreach(var type in _types)
             {
               var typeCatalog = new TypeCatalog(type);
               var part = typeCatalog.Parts.FirstOrDefault();
  
               if (part == null) continue;
  
               if (typeof(IController).IsAssignableFrom(type))
               {
                 part = new 
                   ControllerPartDefinitionDecorator(
                     part, 
                     type, 
                     type.Name, 
                     type.Namespace);
               }
  
               partsCollection.Add(part);
             }
  
             Thread.MemoryBarrier();
  
             _parts = partsCollection.AsQueryable();
           }
         }
       }
  
       return _parts;
     }
   }

What we’re doing here is: for each type discovered – is a MEF part – we also check if it happens to be a controller. If so, we decorate that part to add an additional export. This export has all the extra metadata that we need to query for that controller later on.

Wiring

As we’re using MEF for extensibility we might just as well use it for typical composition – as long as our requirements are low compared to the use of a full fledge IoC Container.

 [Export, PartCreationPolicy(CreationPolicy.NonShared)]
 public class ForumController : BasePublicController
 {
   private readonly ForumService _forumService;
  
   [ImportingConstructor]
   public ForumController(ForumService forumService)
   {
     _forumService = forumService;
   }
  
   public ActionResult Index()
   {
     ViewData["forums"] = _forumService.GetEnabledForumsRecentActivity();
  
     return View(ViewRoot + "Index.aspx");
   }
 }

In this controller we import (depend) ForumService, which itself depends on IForumRepository. If you don’t mind the extra noise of attributes, MEF can also cover this aspect of your app – but remember, it wasn’t built to be an IoC Container as the ones out there – more on that in another day.

Conclusion

In the user-level integration MEF plays very well with ASP.NET MVC to enable extensibility on web apps. Of course, this is not the whole story. Like I mentioned, database schema updates/version, making additional content available (js, css, images) and UI Composition are challenging things themselves, but solvable – been there, done that.

With MEF you have one less thing to worry about in this challenging problem space.

Update: Kamil spot the fact that my decorator is leaving out one member, and that essentially makes it ignore the creation policy (which is attached to the part definition, not exports). Sample updated.

MvcWithMEF.zip

Comments

  • Anonymous
    April 24, 2009
    aren't you the guy who whined and cried about ben not posting samples with best-practices?

  • Anonymous
    April 24, 2009
    Looks good.  I would consider augmenting the sample to allow dropping in new extension dlls without restarting the app pool, for the oh snap! factor.

  • Anonymous
    April 24, 2009
    @Bill, thanks Bill. I didnt want to use the FileWatcher as it would increase complexity, but totally doable, yeah. @markus, yes I am. What violations of best practices I'm being accused? :-)

  • Anonymous
    April 24, 2009
    Awesome code sample, I am definitley going to roling things out with this.

  • Anonymous
    April 26, 2009
    If MEF is not an IoC container why use it as an IoC container? What are the pros and cons of using MEF in this instance compared to say Windsor/StructureMap? I am very confused to how Microsoft envisions MEF to be used in business apps, i know it is ment as the fundamental plugin framework for MS products.

  • Anonymous
    April 27, 2009
    Hi Torkel, MEF isnt a IoC Container like Windsor or StructuredMap, rather it is an IoC Container built for a very particular scenario: open ended extensibility. It can still be used as a standard container as long as you're OK with its limitations when it is used as such. Krzystoff is preparing a blog post on the subject. Thanks

  • Anonymous
    May 04, 2009
    I have problem?

  1. I created empty constrsutor in HomeController.
  2. I put new breakpoint in this constructor
  3. Many times I refresh page home in webbrowser
  4. I got only 1 hit in breakpoint :-( It is ok?
  • Anonymous
    May 04, 2009
    In your sample you should add in ControllerPartDefinitionDecorator :-): public override IDictionary<string, object> Metadata {  get  {    return _inner.Metadata;  } }  

  • Anonymous
    May 04, 2009
    Thanks a lot, Kamil. Sample updated :-)

  • Anonymous
    May 11, 2009
    I love how simple this module/plugin approach is using MEF. And hope the ASP.NET team bring out some Previews using MEF with MVC in the near future. And using vanilla Import/Export attributes. I think it would be a shame if ASP.NET MVC added too many layers and custom attributes on top of MEF. Is there a way to author the embedded Views for the Extensions with designer support in Visual Studio?

  • Anonymous
    May 15, 2009
    I created a bootstrapper which allows my IModuleInstaller to add its own view root to my ExtensionWebFormViewEngine. This is my Global.asax: protected void Application_Start()        {            RegisterRoutes(RouteTable.Routes);            ExtensionsBootstrapper.Attach(Assembly.GetExecutingAssembly());        } This is my Bootstrapper public class ExtensionsBootstrapper    {        public static void Attach(Assembly mvcAssembly)        {            HostingEnvironment.RegisterVirtualPathProvider(new AssemblyResourceVirtualPathProvider());            ComposablePartCatalog catalog = new CatalogBuilder()                .ForAssembly(typeof(IModuleInstaller).Assembly)                .ForMvcAssembly(mvcAssembly)                .ForMvcAssembliesInDirectory(HttpRuntime.BinDirectory, Constants.ExtensionsDllSearchPattern).                Build();            var container = new CompositionContainer(catalog);            Collection<IModuleInstaller> modules = container.GetExportedObjects<IModuleInstaller>();            InitializeModules(modules);            ControllerBuilder.Current.SetControllerFactory(new MefControllerFactory(container));        }        private static void InitializeModules(IEnumerable<IModuleInstaller> installers)        {            var viewEngine = new ExtensionWebFormViewEngine();            foreach (IModuleInstaller installer in installers)            {                installer.InstallRoutes(RouteTable.Routes);                installer.InstallViewLocations(viewEngine);            }            viewEngine.Build();            System.Web.Mvc.ViewEngines.Engines.Clear();            System.Web.Mvc.ViewEngines.Engines.Add(viewEngine);        } And my View Engine public class ExtensionWebFormViewEngine : WebFormViewEngine    {        private readonly IList<string> _viewLocationFormats;        public ExtensionWebFormViewEngine()        {            _viewLocationFormats = new List<string>                                       {                                           "~/Views/{1}/{0}.aspx",                                           "~/Views/{1}/{0}.ascx",                                           "~/Views/Shared/{0}.aspx",                                           "~/Views/Shared/{0}.ascx"                                       };            MasterLocationFormats = new[]                                        {                                            "~/Views/{1}/{0}.master",                                            "~/Views/Shared/{0}.master"                                        };        }        public void AddViewLocation(string viewRoot)        {            _viewLocationFormats.Add(viewRoot + "/{1}/{0}.aspx");            _viewLocationFormats.Add(viewRoot + "/{1}/{0}.ascx");            _viewLocationFormats.Add(viewRoot + "/Shared/{0}.aspx");            _viewLocationFormats.Add(viewRoot + "/Shared/{0}.ascx");        }        public void Build()        {            ViewLocationFormats = _viewLocationFormats.ToArray();            PartialViewLocationFormats = _viewLocationFormats.ToArray();        }    }

  • Anonymous
    June 18, 2009
    Great Article! How about the possibility to add this example to mef codeplex site and extend ? In this case are possible and how (i am a newbie) add a page to upload my plugin and refresh the catalog?

  • Anonymous
    June 19, 2009
    This is great for pages.  Would you be able to show how this changes if I wanted to have plugins that were only partial views / i.e. hosting multiple pluggable "widgets" on a single page?

  • Anonymous
    June 28, 2009
    I have problem too?

  1. I created Index Action in HomeController.

   [Export, PartCreationPolicy(CreationPolicy.NonShared)]    public class HomeController : BasePublicController    {        public ActionResult Index(string aaa)

  1. I put new breakpoint in this Action
  2. Many times I refresh page home in webbrowser and different aaa value,I got the first enter value;
  • Anonymous
    July 07, 2009
    I'm having the same issue.  I keep getting the value from the very first call.  Somehow it is getting cached.

  • Anonymous
    September 07, 2009
    If you use strongly typed view your sample not work!

  • Anonymous
    October 22, 2010
    The comment has been removed

  • Anonymous
    November 08, 2010
    Any chance this could be updated to MVC 3?