Building a Composite ASP.NET MVC Application with Pluggable Areas from External Projects and Assemblies

Update: 10/25/2012

  1. Resolved rendering issues from a Plugin, when controllers names are not unique
  2. Added support for strongly typed views for Plugin views to support Entity Framework scaffolding and/or model binding
  3. Decommissioned custom Razor View Engine, moving to convention over configuration, approach
  4. Updated (sourcecode) sample application download link with latest code base

I recently did a post on Building a Composite MVC3 Application with Ninject, this post we will achieve the same goals with a much more simplistic approach using MV3 Areas.

This provides separation of concerns and loose coupling, helping you to design and build applications using loosely coupled components that can evolve independently which can be easily and seamlessly integrated into the overall application. These types of applications are known as composite applications.

So the problem is that the MVC3 runtime only scans the current executing assembly for IControllers, classes that implement AreaRegistration (to load and register MVC Areas), etc. The key here is to get the MVC3 runtime to scan other assemblies as well so that we can have external projects/assemblies so that we can scale the application horizontally without creating one large monolithic web project which also serves lends it self well when having multiple teams working in parrallel on the same project.

So let’s start! How do we get the MVC runtime to scan external assemblies as it does witht he current executing assembly of our Web project/assembly?

Open up your AssemblyInfo.cs class and add this attribute at the very end:

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Web;
using MvcApplication8.Web;

// General Information about an assembly is controlled through the following 
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MvcApplication8.Web")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("HP")]
[assembly: AssemblyProduct("MvcApplication8.Web")]
[assembly: AssemblyCopyright("Copyright © HP 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible 
// to COM components.  If you need to access a type in this assembly from 
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("cdb134c0-92a4-4fe4-a7dc-6ea3e4a88bcc")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version 
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Revision and Build Numbers 
// by using the '*' as shown below:
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

[assembly: PreApplicationStartMethod(
  typeof(PluginAreaBootstrapper), "Init")]

Note: This is letting the runtime know that before loading the MVC3 Web project assembly please invoke the “Init” method the class named “PluginAreaBootStrapper” where we will dynamically scan for our MVC3 Plugin assemblies so that the MVC runtime can scan them as well
http://msdn.microsoft.com/en-us/library/system.web.preapplicationstartmethodattribute.aspx.

Let’s take a look at what the PluginAreaBootstrapper.Init() method is doing:

    public class PluginAreaBootstrapper
    {
        public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();

        public static List<string> PluginNames()
        {
            return PluginAssemblies.Select(
                pluginAssembly => pluginAssembly.GetName().Name)
                .ToList();
        }

        public static void Init()
        {
            var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");

            foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll"))
                PluginAssemblies.Add(Assembly.LoadFile(file));

            PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
        }
    }

Note: We are simply scanning the Areas directory in our main MV3 web app for any plugin assemblies that we are dropping the the [Areas] (MvcApplication8.Web/Areas) folder and adding our external Plugin assemblies with BuildManager.AddReferencedAssembly (http://msdn.microsoft.com/en-us/library/system.web.compilation.buildmanager.addreferencedassembly.aspx) before our main(host) MVC3 Web App gets started.

You can setup break points on PluginAreaBootstrapper.Init() and Global.asax.cs.ApplicationStart() and see that the PluginAreaBootstrapper.Init() (where we are loading our external Plugin assemblies) method is invoked before Global.asax.cs.ApplicationStart().

Add a custom Razor View Engine

This is so that our MVC3 application will know where to go (physical location) to retrieve views from our plugins. We are copying each of our plugins to a folder named after the plugin assembly name under the [Areas] folder from our main web app e.g. MvcApplication8.Web/Areas/MvcApplication8.Web.MyPlugin1.


    public class PluginRazorViewEngine : RazorViewEngine
    {
        public PluginRazorViewEngine()
        {
            AreaMasterLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            AreaPartialViewLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            var areaViewAndPartialViewLocationFormats = new List<string>
            {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            var partialViewLocationFormats = new List<string>
            {
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            var masterLocationFormats = new List<string>
            {
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            foreach (var plugin in PluginAreaBootstrapper.PluginNames())
            {
                masterLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
                masterLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
                masterLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/Shared/{1}/{0}.cshtml");
                masterLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/Shared/{1}/{0}.vbhtml");

                partialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
                partialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
                partialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/Shared/{0}.cshtml");
                partialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/Shared/{0}.vbhtml");

                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.cshtml");
                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.vbhtml");
                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.cshtml");
                areaViewAndPartialViewLocationFormats.Add(
                    "~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.vbhtml");
            }

            ViewLocationFormats = partialViewLocationFormats.ToArray();
            MasterLocationFormats = masterLocationFormats.ToArray();
            PartialViewLocationFormats = partialViewLocationFormats.ToArray();
            AreaPartialViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
            AreaViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
        }
    }

Note: Lines 45-77 is where we iterate through each of our plugin assemblies and register their physical location(s) of their views.

Let’s create our external VS MVC3 Web Project for our Plugin

  1. Add a MVC3 Project to our solution e.g. MVCApplication8.Web.MyPlugin1
  2. Create a controller named MyPlugin1Controller.cs

    #region
    
    using System.Web.Mvc;
    
    #endregion
    
    namespace MvcApplication8.Web.MyPlugin1.Controllers
    {
        public class MyPlugin1Controller : Controller
        {
            public ActionResult Index()
            {
                return View();
            }
        }
    }
  3. Add a View (Index.cshtml)

    
    @{
        ViewBag.Title = "Index";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    <h2>Hello! This is a Razor View from MvcApplication8.Web.MyPlugin1</h2>
    
    
    
  4. Add some xcopy commands to our Plugin project build post events

    MyPlugin1 post build event

    xcopy “$(ProjectDir)\Views” “$(TargetDir)\MyPlugin1\Views\” /s /i /y
    xcopy “$(ProjectDir)\Areas” “$(TargetDir)\MyPlugin1\Areas\” /s /i /y
    xcopy “$(ProjectDir)\Content” “$(TargetDir)\MyPlugin1\Content\” /s /i /y

    MyPlugin2 post build event

    xcopy “$(ProjectDir)\Views” “$(TargetDir)\MyPlugin2\Views\” /s /i /y
    xcopy “$(ProjectDir)\Areas” “$(TargetDir)\MyPlugin2\Areas\” /s /i /y
    xcopy “$(ProjectDir)\Content” “$(TargetDir)\MyPlugin2\Content\” /s /i /y

    Note: this is to copy our plugin views under the [Areas] folder of our main web app (MvcApplication8.Web), the folder name that the plugins are copied to must match the AreaName being registered by the plugin.

  5. Set the output path for our assemblies to place the compiled assemblies from our plugins under the [Areas] folder in our main host web app e.g. MvcApplication8.Web\Areas.

Now let’s run our application and type in the url prefixed with the controller from our plugin e.g. (http://localhost:48733/MyPlugin1)

Voila.., Our controller and view from an external MVC3 project/assembly loads…!

We’ll go ahead and repeat the steps creating a second plugin just for a sanity check and run the app one more time with the url prefixed with the controller from the second plugin e.g. (http://localhost:48733/MyPlugin2)

Voila.., Our controller and view from our second plugin MVC3 project/assembly loads…!

Quick review on our Solution structure

  1. Our plugin folders that are copied to the [Area] folder of our main MVC3 web app
  2. MyPlugin1 controller from our first plugin
  3. MyPlugin2 controller from our second plugin

Registering Routes from Plugins

In order to Register Routes you have to traditionally do this in the Global.asax.cs RegisterRoutes(RouteCollection routes) method in host web application (MvcApplication.Web). There can only be one Global.asax.cs for any given site. So, how can we register Routes from our Plugins when our host web app and Plugins are now completely decoupled?! Meaning they really have no dependencies and are completely ignorant of eachother (which was our goal to begin with, which was to create a “true” decoupled MVC Pluggable architecture).

This was a great question raised by Basem, so let’s dive into the solution for this. When the MVC runtime starts up our application it scans the current executing assembly as well as our Plugin assemblies thanks to our MvcApplication8.Web.PluginAreaBootstrapper.cs implementation. Meaning it scans for implementations of IController, IDependencyResolver, IControllerFacotry, RazorViewEngines and (whats relevant to our solution at hand) any classes that implement AreaRegistration. If we decompose this implementation you will see that there are two overrides that we must implement:

  • AreaName
  • RegisterArea(AreaRegistration context)

So what we need to do here is add an implementation of AreaRegistration to each of our Plugins.

MvcApplication8.Web.MyPlugin1.MyPlugin1AreaRegistration.cs


namespace MvcApplication8.Web.MyPlugin1
{
    public class MyPlugin1AreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get { return "MyPlugin1"; }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "MyPlugin1_default",
                "MyPlugin1/{controller}/{action}/{id}",
                new {action = "Index", id = UrlParameter.Optional}
                );
        }
    }
}

MvcApplication8.Web.MyPlugin2.MyPlugin2AreaRegistration.cs


namespace MvcApplication8.Web.MyPlugin2
{
    public class MyPlugin2AreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get { return "MyPlugin2"; }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "MyPlugin2_default",
                "MyPlugin2/{controller}/{action}/{id}",
                new {action = "Index", id = UrlParameter.Optional}
                );
        }
    }
}

Now if we set up a few break points in our implementations of AreaRegistration we will notice that when the application initially starts up (you may have to close out any Casini instances or restart your site in IIS, to ensure that it is the absolute first time your app is being started) the MVC runtime will scan both of our Plugin assemblies and will read the AreaName and invoke the RegisterArea methods in them. Great, now we can add any Routes from our Plugins into the body of the RegisterArea(AreaRegistrationContext context) methods…!

Happy Coding…! 🙂

Download sample application: https://skydrive.live.com/redir?resid=949A1C97C2A17906!2600&authkey=!ANP30kAEhO6QSfs

Building a Composite MVC3 Application with Ninject

Update: 05/04/2012 – Preferred alternative approach without using IoC for Plugins (http://blog.longle.net/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas/).

So there’s great support in Prism to building composite applications with Silverlight with the notion of Prism modules. They have a nice discovery approach for dynamically discovering modules when loading XAPs and assemblies during runtime for all the different modules your Silverlight app may need. You can load them during start-up or on demand so that you don’t have to download the entire application at one time.

When building an enterprise MVC application, I wanted to borrow some of the ideas and architecture of the Prism extensible plug and play concepts to provide the ability to build modules (areas) of your MVC application outside of your core MVC application.

For example if you had a MVC application and it’s primary role is an E-Commerce site (core site), however you have this new requirement to build a integration point to show customers all their tracking statuses for their orders. Now you have to ramp up a development team as quick as possible so that they can build out this feature, however you now find yourself giving them a full blown training and overview of your core MVC application, full source control of your entire VS Solution set which could have up to 20-30 projects in it.

Now when devs from the team compile it’s taking forever…! Here we can easily see that our MVC app’s footprint is getting to large and somewhat difficult to manage. So here is now the need to break it down into blocks that can be injected into the app during runtime so that development for these different pieces, blocks, or modules can happen in parallel with your core MVC application (loose coupling, modularity and extensibility).

My preference here is to actually use Mef since I have experience with it and the great support for MVC3 and Mef now. However, I’ve been given the opportunity to be engaged on a project that uses Ninject. With this being the situation at hand I wanted to explore and see if we could borrow the some concepts from Prism, and incorporate them using what was out of the box with Ninject. Some of these concepts include separation of concerns and loosely coupled components (modules) that can evolve independently but can be easily and seamlessly integrated into the overall application, which is also known as a composite application.

With a little bit of Googling I stumbled upon this article http://www.thegecko.org/index.php/2010/06/pluggable-mvc-2-0-using-mef-and-strongly-typed-views/ which was pretty much what we were looking for however it just needed to be ported from MVC2 to MVC3, use Ninject instead of Mef and just a tad bit of polishing.

So after a little bit of poking around with Ninject, I quickly found that Ninject also has a notion of Modules, NinjectModules, or classes that implement the INinjectModule interface. You can ask the Ninject Kenerl (container) to scan an assembly and it will scan for all any classes that implement INinjectModule, this can be done directly by implementing the actual interface or inheriting their out of the box NinjectModule abstract class.

So rather than starting from ground zero here, I’m going to continue with the project from my last post http://blog.longle.net/2012/02/15/wrapping-the-ninject-kernel-with-servicelocator/.

Quick breakdown on the VS Solution structure:

  • MvcApplication

    This is our main MVC application which will host all the Plugins

  • MvcApplication.Plugin.Framework

    This is a C# class library where all the plugin infrastructure classes will reside

  • MvcApplicationA.Plugin

    Example PluginA

  • MvcApplicationA.Plugin

    Example PluginB

  • MvcApplicationA.Plugin

    Example PluginB

Let’s take a quick look at some of the classes in the MvcApplication.PluginFramework VS Project.

  • MyController.cs (implements IMyController)

    All controllers from plugins will need to inherit MyController, this provides some meta-data so that we can infer the correct type of a plugin Controller from our IoC that was requested.

  • MyPlugin.cs

    There will be one class in each plugin that must inherit MyPlugin, this class provides meta-data about the plugin, e.g. PluginName, AssemblyName, etc.. It also gives us the overidable Load operation where we can setup our Ninject bindings e.g. Controller and plugin bindings.

  • MyControllerFactory.cs

    We inherit the DefaultControllerFactory and override the GetControllerType method so that when a user is routed to a controller that is in a plugin, we can help the MVC runtime derive what it’s type is by scanning our IoC for registered instances for that a given Controller and return it’s type.

    
        public class MyControllerFactory : DefaultControllerFactory
        {
            protected override Type GetControllerType(RequestContext requestContext, 
                string controllerName)
            {
                var controllerType = base.GetControllerType(requestContext, controllerName);
    
                if (controllerType == null)
                {
                    var controller = ServiceLocator.Current.GetAllInstances<IController>().ToList()
                    .OfType<IMyController>()
                    .SingleOrDefault(c => c.ControllerName == controllerName);
    
                    if (controller != null)
                    {
                        return controller.GetType();
                    }
                }
    
                return controllerType;
            }
        }
    
    
  • MyRazorViewEngine (MyViewEngine.cs)

    Custom RazorViewEngine so that we can properly return Views *.cshtml (Razor) that have been copied to our Plugins directory in our main app (MvcApplication/Plugins).

  • MyWebFormEngine (MyViewEngine.cs)

    Custom WebFormViewEngine so that we can properly return MVC *.aspx (non-Razor) Views that have been copied to our Plugins directory in our main app (MvcApplication/Plugins).

  • MyPluginBootstrapper.cs and yes, I borrowed the name from Prism :p

    • Scans the “MvcApplication/Plugin” directory and loads all of our plugins
    • Register’s any custom routes from our plugins
    • Register’s our MyControllerFactory with the MVC runtime
    • Register’s our MyWebFormViewEngine with the MVC runtime
    • Register’s our MyRazorViewEngine with the MVC runtime
    
        public class MyPluginBootstrapper : NinjectModule
        {
            private const string _pluginPath = "Plugins";
            private readonly string _fullPluginPath;
            private const string _defaultMaster = "Site";
            private const string _defaultRazorMaster = "_Layout";
    
            public MyPluginBootstrapper()
            {
                _fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _pluginPath);
            }
    
            public override void Load()
            {
                var assemblies = new List<Assembly>();
    
                // Discover  Modules in plugin directory (e.g. site/plugins)
                foreach (var file in Directory.EnumerateFiles(_fullPluginPath, "*.Plugin.dll"))
                    assemblies.Add(Assembly.LoadFile(file));
    
                Kernel.Load(assemblies);
    
                //  plugins discovery
                var plugins = new List<IMyPlugin>();
                foreach (IMyPlugin plugin in ServiceLocator.Current.GetAllInstances<IMyPlugin>())
                {
                    plugins.Add(plugin);
                    plugin.RegisterRoutes(RouteTable.Routes);
                }
    
                // Register ControllerFactory with site
                IControllerFactory myControllerFactory = new MyControllerFactory();
                ControllerBuilder.Current.SetControllerFactory(myControllerFactory);
    
                // Setup ViewEngines
                var myWebFormViewEngine = 
                    new MyWebFormViewEngine(_pluginPath, plugins, _defaultMaster);
    
                var myRazorViewEngine = 
                    new MyRazorViewEngine(_pluginPath, plugins, _defaultRazorMaster);
    
                // Register ViewEngines with site
                ViewEngines.Engines.Clear();
                ViewEngines.Engines.Add(myWebFormViewEngine);
                ViewEngines.Engines.Add(myRazorViewEngine);            
            }
        }
    
    

So what’s all required to setup an MVC plugin now?

  1. Create a regular MVC3 app project to be the plugin
  2. Assembly name must match this pattern *.Plugin.dll, yes we are using convention for this.
  3. Must have one class that inherits MyPlugin.cs

    
        public class PluginA : MyPlugin
        {
            public override void Load()
            {
                Bind<IMyPlugin>().To<PluginA>();
                Bind<IController>().To<PluginAController>()
                    .Named(GetControllerName<PluginAController>());
            }
        }
    
    
  4. Setup binding for the plugin e.g. IMyPlugin -> PluginA

    
        public class PluginA : MyPlugin
        {
            public override void Load()
            {
                Bind<IMyPlugin>().To<PluginA>();
                Bind<IController>().To<PluginAController>()
                    .Named(GetControllerName<PluginAController>());
            }
        }
    
    
  5. Setup bindings for any Controllers for the plugin e.g. IController -> PluginAController
    
        public class PluginA : MyPlugin
        {
            public override void Load()
            {
                Bind<IMyPlugin>().To<PluginA>();
                Bind<IController>().To<PluginAController>()
                    .Named(GetControllerName<PluginAController>());
            }
        }
    
    
  6. All plugin Controllers must inherit MyController.cs
    
        public class PluginAController : MyController
        {
            public ActionResult Index()
            {
                return View();
            }
        }
    
    

Wrapping up, we have addressed the following concerns:

  • Loose coupling
  • Separation of concerns
  • Application modularity
  • Building our application from partitioned components
  • IoC & Dependency Injection
  • ServiceLocation with ServiceLocator and/or Ninject’s IKernel
  • Composite pattern

As requirements changed and the project matures, it will be helpful that we can change parts of the application without having these changes cascade throughout the system. Modularizing an application allow you to build application components separately (and loosely coupled) and to change whole parts of your application without affecting the rest of your code base.

Happy coding…! 🙂