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
Walkthrough
You may start by running the app. You should see the following on your browser:
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.
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. ThanksAnonymous
May 04, 2009
I have problem?
- I created empty constrsutor in HomeController.
- I put new breakpoint in this constructor
- Many times I refresh page home in webbrowser
- 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?
- I created Index Action in HomeController.
[Export, PartCreationPolicy(CreationPolicy.NonShared)] public class HomeController : BasePublicController { public ActionResult Index(string aaa)
- I put new breakpoint in this Action
- 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 removedAnonymous
November 08, 2010
Any chance this could be updated to MVC 3?