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

About these ads

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

  1. We absolutely love your blog and find most of your post’s to be exactly I’m looking for.
    Does one offer guest writers to write content for you personally?
    I wouldn’t mind publishing a post or elaborating on most of the subjects you write concerning here.
    Again, awesome site!

  2. Hello All,
    I need a help here desperately. I was using your approach and all of a sudden something wrong happened. Probably issue was there and I detected now only. During the first time launch of my web site, my application pool is getting shut down and started again and this fails my application first time. From second time onwards, everything works fine. I was struggling to understand what is causing the application to shutdown and I found the blog from ScottGu(http://weblogs.asp.net/scottgu/433194). After putting this code, I am getting the below text during application shutdown.\

    _shutDownMessage=Change in C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\dicomconfigwebui\af6082a8\41f735a8\hash\hash.web
    HostingEnvironment initiated shutdown
    HostingEnvironment caused shutdown

    _shutDownStack= at System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
    at System.Environment.get_StackTrace()
    at System.Web.Hosting.HostingEnvironment.InitiateShutdownInternal()
    at System.Web.Hosting.HostingEnvironment.InitiateShutdownWithoutDemand()
    at System.Web.HttpRuntime.ShutdownAppDomain(String stackTrace)
    at System.Web.Compilation.BuildManager.OnWebHashFileChange(Object sender, FileChangeEvent e)
    at System.Web.DirectoryMonitor.FireNotifications()
    at System.Web.Util.WorkItem.CallCallbackWithAssert(WorkItemCallback callback)
    at System.Web.Util.WorkItem.OnQueueUserWorkItemCompletion(Object state)
    at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
    at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
    at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
    at System.Threading.ThreadPoolWorkQueue.Dispatch()
    at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

    It seems BuildManager.AddReferencedAssembly() is causing the application to shutdown. Any idea what is wrong here. My plug-in assembly has reference to some dlls which are already dependencies of the parent website. is this a concern. I have AssemblyResolver(AppDomain.CurrentDomain.AssemblyResolve event) and it is working fine.

  3. First, thank you! This is what I’ve been looking for, and thank you to all that have preceded me, your efforts have lessen mine. Still, I have a question (problem). I’ve tried many variations and I’ve done some ‘googling'; however, and this is certainly my lack of MVC experience, but how can I get my (plugin) views to recognize style sheets and scripts?

    In other words, I want my views to use the style sheets in their corresponding external assemblies, and not the main assembly? It seems that no matter what I try, my plug-in view refuses to recognize my local (assembly) style sheets and scripts.

    Thanks in advance,
    Glenn

    • Yes, it’s a bit weird replying to my own post; however, after continuous reading of this (wonderful) posting, I recognized that my question is similar to Novkovski Stevo Bato’s; dated September 12, 2013 at UTC (see below); however, even after specifying my plugin (name) as suggested (below), It’s still not working (for me).

  4. I was looking for a mvc modular solution for a while and I was really suprised that there is no legit complete working solution. I thought modular is popular thing nowaday.
    And then I found this article. Really great. My big thank to you.
    But at the time I read this, it is almost over a year now. And MVC has upgraded to version 5.
    So I wondering if this solution is still working. Any new and simple modular way for MVC5 ?. Or this is just simple the best now

  5. Hi Long, this is a wonderful article.

    I have a quick question.

    From your approach, is it possible to compile those plug-in project separated, and manually copy and paste the generated dll files directly into the Area folder? Does it require a sever restart?

    The WordPress’s plugin-in/widget system is very cool. People can just download and install their desired plugin/widget within couple clicks.

    By following your approach, is it possible to get the similar function?

    Thank you very much.

  6. Excellent beat ! I would like to apprentice at the same time as you amend your website, how could i subscribe for a weblog site?
    The account helped me a acceptable deal. I had been a little bit acquainted of this your broadcast offered shiny transparent idea

  7. Thanks for sharing wonderful article. I need your help. How I can register Area wise dependency injection. I am using Ninject in my project. I want to inject dependency based on area. How i can achieve this..

  8. Hi, I am going to use Unity’s IOC Container to do dependency injection. I usually will create my containers in a boostrapper.cs file and initialize it in the global.asax.cs. Since there can only be one global.asax.cs per site, I assume I would place it in the core or main project. It won’t work unless I add a reference to the PluginIn project’s assembly. Is this the preferred way or is there another way ? Thanks!

    • Yes you only need one Global.asax.cs in your core (host) web project, the other ones won’t even be executed when the app starts, you can effectively delete them.

  9. I like the helpful information you provide in your articles.
    I’ll bookmark your weblog and check again here regularly.
    I am quite sure I’ll learn a lot of new stuff right
    here! Good luck for the next!

  10. Greetings! I know this is kind of off topic but I was wondering which blog platform are you using for this site? I’m getting sick and tired of WordPress because I’ve had issues with hackers and I’m looking at options for another platform. I would be fantastic if you could point me in the direction of a good platform.

  11. Have you ever thought about creating an e-book or guest authoring on other sites? I have a blog centered on the same information you discuss and would love to have you share some stories/information. I know my readers would value your work. If you’re even remotely interested, feel free to shoot me an e-mail.

  12. Wonderful goods from you, man. I’ve understand your stuff previous to and you’re just extremely magnificent. I really like what you’ve acquired here, certainly like what you are saying and the way in which you say it. You make it entertaining and you still care for to keep it smart. I can not wait to read far more from you. This is really a wonderful web site.

    • Hi AcidRaZor,

      Typically we’ve used this architecture for building our our larger systems, in the beginning we modularize the system e.g. Accounting, HumanResources, Acquisitions, etc. or for a fairly decent sized E-Commerce system e.g. Order Pipeline, Invoicing, Distributor, Inventory, etc. Each of these are designed to be loosely coupled to a certain degree, and for these modules that have front facing applications (UI/UX) we use this architecture and create each of them a first class citizen MVC project they can build their modules into. We’ve even often have some of these MVC Projects (Plug-ins) out sourced. Obviously each of these MVC plug-ins would all use common frameworks among them, Unit Of Work, Repository, Logging, Caching, Utilities, Entities. We also have implemented ServiceLocator Pattern so that each of the Plug-ins can access certain instances/objects during run-time. How you leverage this architecture for multi-team projects really varies and depends on many factors e.g. team composition, skill set’s, geography, etc.

  13. Thanks for this helpful post!

    I checked out the sample application and noticed that when you browse to “http://localhost/MyPlugin1/” (instead of http://localhost/MyPlugin1/Home using the provided link), MyPlugin1Controller.Index() gets called, but then it will throw an InvalidOperationException:

    The view ‘Index’ or its master was not found or no view engine supports the searched locations. The following locations were searched:
    ~/Views/MyPlugin1/Index.aspx
    ~/Views/MyPlugin1/Index.ascx
    ~/Views/Shared/Index.aspx
    ~/Views/Shared/Index.ascx
    ~/Views/MyPlugin1/Index.cshtml
    ~/Views/MyPlugin1/Index.vbhtml
    ~/Views/Shared/Index.cshtml
    ~/Views/Shared/Index.vbhtml

    Does anyone know how to fix this, so that an url like http://localhost/MyPlugin1 will work?

    • You’ll need to configure your routing. In “MyPlugin1AreaRegistration” check how you’ve setup the routing for this plugin.

      Try change it from:

      context.MapRoute(
      name: “MyPlugin1″,
      url: “MyPlugin1/{controller}/{action}/{id}”,
      defaults: new { action = “Index”, id = UrlParameter.Optional },
      namespaces: new[] { “MvcApplication8.Web.MyPlugin1.Controllers” }
      );

      to

      context.MapRoute(
      name: “MyPlugin1″,
      url: “MyPlugin1/{controller}/{action}/{id}”,
      defaults: new { controller = “myplugin1″, action = “Index”, id = UrlParameter.Optional },
      namespaces: new[] { “MvcApplication8.Web.MyPlugin1.Controllers” }
      );

      Incase you missed it – the controller is now defaulted to “myplugin1″.
      You can obviously default it to which ever controller you prefer/require.

  14. Hi, I found this post very helpful in setting up a composite app using MVC 5 and VS 2013 RC. Howvever, I wanted to know how do I reference (use) models from a child project in the main web project. This is since VS gives errors if we write ” using ….” statements while referring to child projects. It says it has already imported the projects in main app. So I am unable to use models/objects from child project to develop code in main project. Is there a way to get this going? Thanks a lot.

    • I’m not sure you want to do this, one of the design philosophies was to decouple these external MVC plug-ins and the the host app. Meaning the host app, should be completely ignorant of the plug-ins and internals. This way you could have development teams build modules/plugins in almost complete isolation. Maybe, I’m not understanding clearly what the your design goal is here, please ellaborate a bit more and maybe I could provide some help.

  15. Ok so I’ve found my solution:

    The first problem was you’re not adding the your newly created ViewEngine into MVC. So in order to do this, you need to add this line to you Application_Start():

    ViewEngines.Engines.Add(new PluginRazorViewEngine());

    Second problem was using strongly typed views. As you said in comments, your example will work as long as you don’t use typed views (I failed to read your comment before), but you don’t explain how to solve it!
    This one was hard, and I resorted to http://www.thegecko.org/2010/06/pluggable-mvc-2-0-using-mef-and-strongly-typed-views/ . In short, the answer is to override MVC’s assembly resolution mechanism. This I made it fisrt by adding this line to Application_Start():

    AppDomain.CurrentDomain.AssemblyResolve += PluginAssemblyResolve;

    Then let’s define this handler:

    private Assembly PluginAssemblyResolve(object sender, ResolveEventArgs resolveArgs)
    {
    Assembly[] currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();

    // Check we don’t already have the assembly loaded
    foreach (Assembly assembly in currentAssemblies)
    {
    if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
    {
    return assembly;
    }
    }

    // Load from directory
    return LoadAssemblyFromPath(resolveArgs.Name, _fullPluginPath);
    }

    private static Assembly LoadAssemblyFromPath(string assemblyName, string directoryPath)
    {
    foreach (string file in Directory.GetFiles(directoryPath))
    {
    Assembly assembly;

    if (TryLoadAssemblyFromFile(file, assemblyName, out assembly))
    {
    return assembly;
    }
    }

    return null;
    }

    private static bool TryLoadAssemblyFromFile(string file, string assemblyName, out Assembly assembly)
    {
    try
    {
    // Convert the filename into an absolute file name for
    // use with LoadFile.
    file = new FileInfo(file).FullName;

    if (AssemblyName.GetAssemblyName(file).Name == assemblyName)
    {
    assembly = Assembly.LoadFile(file);
    return true;
    }
    }
    catch
    {
    }

    assembly = null;
    return false;
    }

    I hope this helps someone.

    • Thanks Fabzter for posting this, this question has been raised a few times, just haven’t had time to update this post. I’m sure other readers will benefit from this answer since strongly typed Views are a common development use case within the plug-ins.

  16. Hi Le!
    This is such a great post, I was exactly trying to implement something like this. I followed your article and I’ve got almost everything working but I stumbled across a problem and I can’t seem to find a solution:

    I get controller routing right, but whenever the view cannot be found. An exception is thrown saying the view could not be found (I made only one plugin and it’s creatively called “Plugin”):
    The view ‘Index’ or its master was not found or no view engine supports the searched locations. The following locations were searched:
    ~/Areas/Plugin/Views/Plugin/Index.aspx
    ~/Areas/Plugin/Views/Plugin/Index.ascx
    ~/Areas/Plugin/Views/Shared/Index.aspx
    ~/Areas/Plugin/Views/Shared/Index.ascx
    ~/Views/Plugin/Index.aspx
    ~/Views/Plugin/Index.ascx
    ~/Views/Shared/Index.aspx
    ~/Views/Shared/Index.ascx
    ~/Areas/Plugin/Views/Plugin/Index.cshtml
    ~/Areas/Plugin/Views/Plugin/Index.vbhtml
    ~/Areas/Plugin/Views/Shared/Index.cshtml
    ~/Areas/Plugin/Views/Shared/Index.vbhtml
    ~/Views/Plugin/Index.cshtml
    ~/Views/Plugin/Index.vbhtml
    ~/Views/Shared/Index.cshtml
    ~/Views/Shared/Index.vbhtml

    AAAAND, I’m pretty lost at this point. I doubled checked: the view is there and listed ( ~/Areas/Plugin/Views/Plugin/Index.cshtml). I hope you can help me understand what’s happening.

  17. Why we cant use layout files from plugin views folders.

    For example:

    MyPlugin1/Views/_ViewStart.cshtml

    @{
    Layout = “~/Views/Shared/_Layout.cshtml”;
    }

    but razor still get _Layout.cshtml from core project. If you make _Layout2.cshtml in MyPlugin1/Views/Shared and then try to use

    @{
    Layout = “~/Views/Shared/_Layout2.cshtml”;
    }

    in MyPlugin1/Views/_ViewStart.cshtml , you will get error that file does not exist.

    Any solution?

    • I don’t see the Plugin name in your path, it should look like:

      @{
      Layout = “~/MyPlugin/Views/Shared/_Layout2.cshtml”;
      }

  18. Hello! Le:
    greate post!and I have a questions.

    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);
    }

    this code is load assemblies from some where.and hao about loading sequence?
    what if a PluginA depend on PluginB,and this code exactly loading PluginA first?
    谢谢!

    • Thanks wangtuyao, all you have to do is make sure whatever dependencies each of the Plugins need they are referenced, when you compile they are all copied to the Host project’s bin folder, therefore any dependencies from any of the Plugins should readily be available during run-time, you shouldn’t have any issues.

    • Hi Fred,

      This is what the post build event should be doing, which is copy all the *.plugin.dll’s to the host app’s bin directory.

  19. Hi,
    First of all thanks a lot for this wonderful article. I tried the same approach built a small prototype for my project. I am facing one issue. How can we return a partial view from plug-in. I prefer the plug-in author to create some partial views based on the model data that I pass(from plug-in host) and those will shown in main web site. My prototype is available in skydrive as https://skydrive.live.com/#cid=EAA26C1BAE3050B8&id=EAA26C1BAE3050B8!128. It will be a great help, if you can tell me how to return a partial view which is defined inside a plug-in.

    • You could return a partial with an @Html.Action(“ActionName”, “ControllerName”, new {area=”AreaName” })

      Your action would return the partial view with a return PartialView(“partialviewname”);

      Let me know if you need more info.
      -A

  20. Hi,
    I was looking the other day for a clean open source cms on mvc platform as a base for building something of my own, with support for plugable features and extensibility and my small research ended up not in a cms but rather than a well known eshop, NopCommerce. Yeap, not only they worked on ASP.NET all those years but rewrote everything again in ASP.NET MVC and done a great job. Simple, neat and as expected from a typical. NET developer. Their project uses the approach discussed here and anyone that asks him self “ok and now what” should have a look at the infrastructure around loading external mvc components. They even have a great support for localization and use EF code first for DAL. I swear i am not advertising but rather showing happiness that someone is doing a good job applying .NET code and don’t forget what. NET stands for :-).
    Best Regards,
    Panos.

  21. Great Post Le, But it is not working with strongly typed views from Plugins. I have seen your comments sayin you have updated the Code, but could not find it your skydrive? Could you please point me where can I get that

  22. Great post! Now starting MVC development and my app design hinged on finding something like this!

  23. Hi Le, very interesting article. I have a pair or three questions thou.
    The custom Razor view engine you describe in the post. It’s intentionally removed from the sample code?
    Logically I tried it myself and I’ve been proactive enough (or tempted) to name my test plugin “Sales”. So It rendered completely unuseable. Stupid my, but I find it a great handicap.
    Something also shocked me it’s that it’s all about decoupling and on top of the sample there’s has a “fixed-strings-driven” menu. And you can’t directly access to plugins via routes untils you accessed at least one time through the menu (so navigator has cache).

    No pun intended, just pointing where I think some polish is required.
    Congrats, that’s the most complete and better practical example I found on the net about modularity. Thanks.

  24. There is a method to unload a plugin from assembly???

    like BuildManager.AddReferencedAssembly(assembly);

    remove reference??

    Thanks a lot

    • Hi Mounir,

      If you download the sample code there should be different actions (what you are referring to as methods), this would probably be a good starting point to get your working.

  25. Best project!! Good article Le!!!

    What do you suggest for admin area???

    I mean in my project to made an area only for administration and in plugin to add and admin area for the management…

    How can i implement in this project??

    And is it possible to make the view like included resource???

  26. Hello Le,

    Thanks for the great article. One small issue, I am not able to download sample application. Is the link broken? Will you be able to email me the latest sample application.

  27. Hi,I like this article !
    I would like to use it in my project that use Castle.Windsor.
    But

    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
    if (controllerType == null) return null;
    return (IController)container.Resolve(controllerType); // *** HERE ***
    }

    On HERE this error occour..

    “No component for supporting the service MyApp.MyPlugin1.Controllers.HomeController was found”

  28. Hello, I’m trying to implement an MVC 4.5 web solution with dynamically discovered plugins, and among others I found your post. I’m facing a non trivial issue in my attempt, and given your experience in this your opinion might prove very useful, even if the post refers to a previous version.

    Essentially the solution keeps the binaries in a plugins folder, and in the equivalent of your bootstrapper first makes a copy of them into a shadow-copy subfolder and then uses this as the source, so that we can update them without having them blocked by the running app; as for views and other contents, they are simply copied like in your sample.

    The crucial point here is that we want a dynamic discovery and a decent handling of plugin dependencies: so I’m using MEF. The plugins export controllers via MEF, and the factory loads them into a directory catalog from the shadow-copy subfolder. This should work fine, but I am facing issues related to assumptions probably made by MVC about the namespaces used in the areas controllers, which cause my controllers factory to receive a null type to be instantiated, and thus invariably fail even before its code can get into play. Interested readers can find more (with a complete repro solution) in this post: http://stackoverflow.com/questions/13836673/mvc4-mef-plugins-and-controllers-namespaces. Could anyone contribute to a solution?

  29. Hi!
    This is awesome article.
    I have one question! I have build solution with separate projects as plugins(modules),(everything works fine) But in the modules i dont have intellisense. so it is a litle hard to code. How can i fix this, thanks in advance! I use mvc 4, and framework 4. In the modules project i removed the global.asax. Global asax only exist in the “front” project!

    • Hi Philip,

      I don’t think the reason you are not getting intellisense is because of the Plugin architecture. I am getting full intellisense in all our plugins, please see screenshot below. This is from the AdminController in one of the plugins from the sample download on this post.

  30. Hi,

    Thank you VERY much for posting this (brilliant article).

    Just a quick question with routing (only got onto MVC last week):

    If I add an “admincontroller” to MyPlugin1 and another to MyPlugin2, how can I configure the routing to redirect correctly because any access to MyPlugin2/admin is being processed by the MyPlugin1 “admincontoller” (default action on both controllers is index).
    It doesn’t seem to be processing the routing correctly based on the area’s specified?

    Thanks for your help! ;)

    • Hi Orilon, apologize for the late response, was attending TwilioCON 2012.

      Could you please verify that each of the Plugins (Areas) are being registered correctly, and that the Plugin route is unique across all plugins?

      
      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}
                      );
              }
          }
      }
      
      

      In each of the Plugins (Areas) you should have a class that inherits the AreaRegistration class and the property value for “AreaName” needs to be unique across all Plugins (Areas). The MVC runtime will scan for all classes that inherit AreaRegistration during start-up and will use reflection to extract the AreaName, again this has to be unique. I’m suspecting this is causing your problem. Let me know if this helps.

    • Hi Le,

      Yes, my AreaRegistration(s) match your specified recommendation

      The situation I have specifically is:

      /// In my “Blog” plugin I have the following BlogAreaRegistration class
      public class BlogAreaRegistration : AreaRegistration
      {
      public override string AreaName
      {
      get { return “Blog”; }
      }

      public override void RegisterArea(AreaRegistrationContext context)
      {
      context.MapRoute(
      “Blog_default”,
      “Blog/{controller}/{action}/{id}”,
      new { action = “Index”, id = UrlParameter.Optional }
      );
      }
      }

      /// In my Snippets plugin I have the following SnippetsAreaRegistration class
      public class SnippetsAreaRegistration : AreaRegistration
      {
      public override string AreaName
      {
      get { return “Snippets”; }
      }

      public override void RegisterArea(AreaRegistrationContext context)
      {
      context.MapRoute(
      “Snippets_default”,
      “Snippets/{controller}/{action}/{id}”,
      new { action = “Index”, id = UrlParameter.Optional }
      );
      }
      }

      I’ve set the “PluginRazorViewEngine.cs” and “PluginAreaBootstrapper.cs” on my base site exactly as instructed.

      Case Point: I’ve added an “AdminController” with the default “Index” method to each of these plugin’s. When I enter in the following urls “http://localhost:xxxx/snippets/admin/index” I keep ‘landing’ on (routed to) the Blog admin controllers “Index” method.

      It seems that the plugin view engine is building the routes in that order, which is why my blog plugin is resolving before my articles plugin, but the fact that I specified “../snippets/admin…” seems to have no impact on the url/route resolution.

      Any help would honestly be appreciated.

      Regards,
      Chris (Orilon)

    • Orilon, first off good catch, we actually have this running in production, however none of our plugins have the same controller name within them, therefore by coincidence all of our controller names from our plugins are unique, therefore we’ve never ran into this issue.

      I’ve updated the blog and source code. In the new code base I have implemented your use case, each of the plugins in the sample have an AdminController which also resolves to the correct view.

    • Le, you’re a legend!
      Thanks again for this post, and professional response rate!
      Going to apply the changes now. ;-)

    • Hi Le,

      I’ve removed the PluginRazorViewEngine, and adjusted the rest of my project to try match yours but it’s just not resolving other than the HomeControllers in each plugin?

      Can I ask you to revise the changes you made in point form so I can ensure I’ve applied the same on my side?

      If it helps, I have also added my project (1.3mb) for you to see what/where I’m at. ;-)
      Link: https://skydrive.live.com/?cid=f13a4931c81e36a5#cid=F13A4931C81E36A5&id=F13A4931C81E36A5!125

    • hi orilon, can you please verify that the solution I uploaded with the new code base is working and the use case that was implemented with an AdminController is rendering the right view from each of the plugins. Just want to make sure, this is the expected behavior you are looking for. If it is then we know that it’s an issue in your solution and we can troubleshoot from there.

      btw, the best way to get a hold of me real time during the day in CST is to just tweet me, here’s my Twitter Handle @LeLong37.

  31. Great article Le, couple of questions:

    1) Will this work in azure web role?
    2) In the _layout.cshtml in the plugin1/2 you have this code @Html.ActionLink(“About”, “About”, “Home”)…, but this is using the host application action link instead of the loaded plugin, is there a way to fix this? The same is happening with the Content folder reference for the plugin.

    • @Fer: You can use the following code to generate the menu links (see examples at the end)

      public static class MenuHelper
      {
      public static MvcHtmlString TopMenuLink(this HtmlHelper htmlHelper, string linkText, string controller, string action, string area, string anchorTitle)
      {
      var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
      var url = urlHelper.Action(action, controller, new { @area = area });
      var anchor = new TagBuilder(“a”);
      anchor.InnerHtml = HttpUtility.HtmlEncode(linkText);
      anchor.MergeAttribute(“href”, url);
      anchor.Attributes.Add(“title”, anchorTitle);
      var listItem = new TagBuilder(“li”);
      listItem.InnerHtml = anchor.ToString(TagRenderMode.Normal);
      if (CheckForActiveItem(htmlHelper, controller, action, area))
      listItem.GenerateId(“menu_active”);
      return MvcHtmlString.Create(listItem.ToString(TagRenderMode.Normal));
      }

      private static bool CheckForActiveItem(HtmlHelper htmlHelper, string controller, string action, string area)
      {
      if (!CheckIfTokenMatches(htmlHelper, area, “area”))
      return false;
      if (!CheckIfValueMatches(htmlHelper, controller, “controller”))
      return false;
      return CheckIfValueMatches(htmlHelper, action, “action”);
      }

      private static bool CheckIfValueMatches(HtmlHelper htmlHelper, string item, string dataToken)
      {
      var routeData = (string)htmlHelper.ViewContext.RouteData.Values[dataToken];
      if (routeData == null) return string.IsNullOrEmpty(item);
      return routeData == item;
      }

      private static bool CheckIfTokenMatches(HtmlHelper htmlHelper, string item, string dataToken)
      {
      var routeData = (string)htmlHelper.ViewContext.RouteData.DataTokens[dataToken];
      if (dataToken == “action” && item == “Index” && string.IsNullOrEmpty(routeData))
      return true;
      if (dataToken == “controller” && item == “Home” && string.IsNullOrEmpty(routeData))
      return true;
      if (routeData == null) return string.IsNullOrEmpty(item);
      return routeData == item;
      }
      }

      Example:

      @Html.TopMenuLink(“Home”, “Home”, “Index”, “”, “Home”)
      @Html.TopMenuLink(“linktext”, “controller”, “action”, “area”, “anchortitle”)

    • Hi Fer, apologize for the late response, was attending TwilioCON 2012.

      1. This will work fine with Azure WebRole
      2. All links should be generated with the preconceived notion that all plugins are essentially true MVC areas, all we are doing here is separating an MVC area into it’s own project and copying the Area (Plugin) back into the (Host) MVC project after compile. Let me know if this helps.
  32. Hi!
    Firstly, awesome article.

    I am wondering how, possibly using this system, one would have a database abstraction plugin that other plugins would rely on. Plugin dependancy if you will.
    This would allow easy updating of database abstraction without restarting the application.

    • JD, thanks for the feedback, could you elaborate more on your comment? I didn’t quite grasp what the your objective was in terms of database abstraction.

  33. thanks for this helpful article .. it would be great if there is a way to integrate this with Entity Framework code first .as it seems unclear to me how to accomplish such a modular design with it .

    anyway ,very nice article ,thanks!

    • HI Abdu, thanks, this has actually been requested many times, I will try to have this an example view scaffold with EntityFramework 5 sometime this week.

    • Hi Abdu, Entity Framework CodeFirst should work, just make sure your DataContext is in the host (main web) project, and make sure that the default project is set to the host (main web) project in the NuGet Package Manager Console.

  34. Hi,

    Thanks for this excellent solution. I have some question regarding this solution.

    1) If i want to set the route to MyPlugin1 by default i.e if i am running VS 2010 IDE it should be navigated to MyPlugin1 screen automatically.

    2) If i want to redirect to a view available in the main project (MVCApplication.web) using RedirectToAction method from MyPlugin1 view, how can i do that?.

    Thanks

    Jibli

  35. Hello, thanks for your help, but I have another question. I have main database in root project in app_data folder. If my plugin uses databse, how Can I referenced to it ? When , my plugin will contain own databse structure for articles (example) how can I integrate this structure to main database of project ? Thanks !

  36. I´m making pluggble project using asp.net mvc 3 as a bachelor work in my school and I love your tutotial, but I have some questions and I will be glad if you can help me.

    One thing that I haven’t been able to figure out though is how to do this on the fly. Every approach seems to put everything together PreApplicationStart. How could we go about making it so that modules can be downloaded and installed on the fly from an online gallery like wordpress or orchard do? In that usage case it’s not really feasible to restart the whole application every time.

    I want to create system where administrator can enable/disable or install/unistall plugin easily.

    Thank you for your help !

  37. This is probably the best MVC plugin architecture approach I have found…
    One thing that I haven’t been able to figure out though is how to do this on the fly. Every approach seems to put everything together PreApplicationStart. How could we go about making it so that modules can be downloaded and installed on the fly from an online gallery like wordpress or orchard do? In that usage case it’s not really feasible to restart the whole application every time.

  38. Hello I have a question or problem with the composite application you have in this sample. I am getting the message ‘ CS1703: An assembly with the same identity ‘XXX.Components.Loggers, Version=1.0.0.0, Culture=neutral, PublicKeyToken= c13be453998ddf97′ has already been imported. Try removing one of the duplicate references’ when I have a reference to a dll in the main website as well as the same reference in one of the modules (in this case I want to have this logger dll referenced in both. Am I missing a setting here or is there a work around to this

  39. Hi there,
    I like your tutorial for pluggable Areas. Works fine!
    But I have one question: Is it possible to use models with EntityFramework within plugged areas?
    I’m still running into a “cannot find your models”error when I open a view beginning with “@model …”.

    Greetings

  40. If I’m building a CRUD app and using the /AppData folder, will this work if I am creating specific logic in the Plugin? In other words, if I’m doing what I would normally do inside a regular MVC3 area will it work fine?

    • Yes, In theory ins are just MVC Areas that are that geographically live in it’s own project that are wired up (discovered) when the app first starts up.

  41. What about images/css from the plugins? I just tried to fiddle around with the CSS under /Content in the indiviudal plugin and see no change.

    • Digging around I notice that since the Plugin uses the standard MVC3 web app structure, it has Site.css and _Layout.cshtml. If I change it to _PluginLayout.cshtml I get the error that it cannot be found. This is probably because the view engine is only looking at the base site. Any workaround?

    • Should I just use the base site for images/css? Meaning I could just add class id/name in the Plugin area, and then when it gets pulled up on the browser it’s looking in the base site’s /Content folder?

    • This problem is common when trying to separate out MVC structures into their own MVC projects. The two options you generally have are:

      1) Create a URL helper that is driven by web.config
      2) In the project of the module being developed, place your views in the Areas folders so that when it is deployed, all the literal string paths will work in the host MVC application that uses the module as an Area. (Yuck)

      I recommend to use a custom URL helper. You already know that once the plugin is deployed it will be available at “~/Areas/[yourpluginname]/” folder of the host MVC application. So create a URL helper that will allow you to build paths to your module content folder regardless of whether they are sitting at the root or in an area folder. The below has been slapped together so don’t expect it to compile:

      // In a helper file
      public static class ConfigHelper
      {

      public static string GetModulePathRoot()
      {
      return ConfigurationManager.AppSettings["ModulePathRoot"];

      }

      }

      // In the module development MVC app web.config

      // In the host MVC app web.config

      The two appSettings above are examples of settings you might use depending on whether the plugin is in it’s own MVC project being developed, or whether it has been deployed in a host MVC app.

      // In your URL helper extension class
      public static string BuildModuleContentPath(this UrlHelper helper, String resourcePath) {

      var root = ConfigHelper.GetModulePathRoot();
      var areaName = helper.RequestContext.RouteData.DataTokens["area"];

      // Ensure areaName is a string and that it has a slash if set
      if(string.isNullOrEmpty(areaName)) areaName = “”;
      else areaName += “/”;

      return helper.Content(root + areaName + resourcePath);

      }

      Now you can build resource paths within your module views and dynamically set their location based on the web.config file. Note that you will have to include the Url.BuildModuleContentPath code in both projects.

      Also note that the folder name of your plugin in the host areas folder, must be exactly the same as that set as the name of your area in the AreaRegistration file.

      Good luck

    • I really should have offloaded that code into a site that allows me to format and color code. Sorry, but it’s not too much so won’t take long to copy and paste and reformat to understand what it’s actually conveying.

    • Chris, I notice that your XML fragments have been edited out completely by the comments form. Can you add those parts back in?

    • Michael,

      What we’ve done is just simply add a post build event under the plugin’s project properties which copies images, css files to the appropriate folders in the host web app e.g. “host.web/content/”. So for example in our in our MvcApplication.Web.MyPlugin1 project I would create a folder MvcApplication.Web.MyPlugin1/Content/Images/MyPlugin1 and place all my images in it, now when we compile the post build event should copy everything to MvcApplication8.Web/Content/Images/MyPlugin1 and all the images will work fine.

  42. Hey Le, in your opinon what would be an appropriate solution to add new plugin assemblies to the application once it has been started? First thoughts are using the Application_BeginRequest in global.asax and loading newly found assemblies (in the plugin folder) directly into the current AppDomain.

    • I’m not sure how much of a performance impact that may have, as obviously the number of requests a busy website would receive would be much much higher than the frequency of adding new plugins to the plugin directory. Scanning the plugin directory every request is feeling a bit overkill.

    • Chris,

      I would definitely not add this to the Application_BeginRequest. What I would recommend is creating a user upload view. This view could take a packaged (zipped) project which is a Plug-In decompress it (this package could have a manifest file if needed) and copy everything to the approriate places and in post-process for the user upload perform scanning of only the “new uploaded” assemblies.

      Now I’m not sure if the MVC runtime will pick-up and scan newly added assemblies that are added to the AppDomain after the application has started during runtime, if not you will need to restart the site programatically as part of the post-process of the package upload. If you have a chance to test if the MVC runtime will automatically scan new assemblies that are added to the appdomain after the app has started and it succesfully registers all MVC classes that implement IController, ControllerFactory, AreaRegistration, etc.. Let me know your results!

  43. Hey Le, this has to be one of the most helpful posts I’ve read for a long time. I’ve been looking for a way to isolate development of my cms MVC application but still be able to integrate it into the mvc application of client projects so that I ultimately only have one MVC application to put into production. Following your post here I’ve been able to do just that!

    Curious to know whether you could add another tier to this? For example, can plugin.dll’s have their own bootstrapper to pull in plugins, so that you can essentially chain plugin hierarchies together?

    It would be great to be able to add plugins to my CMS (blogs, galleries, etc) which can then itself be added to a client site as a plugin.

    Cheers!

    Chris

    • Thanks for the positive feedback Chris, I “believe” it would be possible to chain Plug-In hiercharies together. In theory if each Plug-In would have it’s own implemenation of the attribute [assembly: PreApplicationStartMethod] in the assemblyinfo.cs with each of the plugins to point to their own implemenation of PluginAreaBootStrapper.Init() method. I haven’t tried chaining all this up, but that could be a start. If you attempt to implement this, let me know if it works!

    • I’ve gotten started. The issue is in identifying which ‘plugin’ you are using when the view is being located (as the area being used in the host MVC application has already been taken up by the CMS, and the view location functionality in ASP.NET doesn’t allow for view within a view type locations out of the box). I’ve written a workaround that is working now, but i’m looking at making it a little cleaner. Will post the results when done!

    • Chris, have you gotten this figured out yet? I’m wanting to create a generic Matter plugin, and then chain it to a specific Matter plugin for each of my services (rather than make 3 separate Matter plugins).

    • Hey Robert. In the end the overhead was not worth it. The current MVC framework does not lend itself to this kind of development very well. The default classes are suited only to one tier of Areas… that is, an area cannot have it’s own areas without re writing a significant amount of the default MVC code that deals with resolving paths and urls. Once you go down this road, you seriously risk fragmenting your application from the main MVC codebase to the point where updating to new versions of MVC will be a nightmare.

      In an ideal world, we would be able to use separate Application Domains to run pluggable MVC architectures, that can be hosted within a main MVC application. The problem here is that at present I do not know of an easy way to serialise a RequestContext in order to pass into the AppDomain from the host MVC project (Controllers must be executed with a RequestContext). Then of course the AppDomain would need to be able to serialise it’s ActionResult to send back to the host which can then send it back to the client who made the request.

      You will also find that trying to sandbox MVC plugins is extremely difficult, as without easily being able to load MVC structures into separate AppDomains, you cannot benefit from Code Access Security. Loading a 3rd party assembly into the main AppDomain gives that code the same security privileges that the host code that you wrote has. (Think along the lines of File.Delete())

      Long story short, from where I see it ASP.NET MVC has a way to go yet in order to allow developers to easily create/maintain modular applications.

  44. Pingback: Building a Composite MVC3 Application with Ninject « Long Le's Blog

  45. Very cool! I downloaded the sample project and it is different than what you have outlined here. For example, there is nothing in the AssemblyInfo file nor is there an “Areas” folder (there is a “Plugin” folder instead). Is the steps here the latest or is the sample the more updated technique. Also, does MVC4 introduce any native features that helps simply the technique even more? Thanks again and great article!

    • Thanks Le, the solution is brilliant!! One caveat I noticed is that controller names cannot be duplicated across the whole solution or it gives an error (Multiple controller names defined for the same name…). This becomes problematic for big teams since we cannot remember each others’ controller names, so I tried adding a namespace and adjusted the route path for plugin apps. Unfortunately, the routes in the plugins’ global.asax are getting ignored completely. Still trying to figure this one out. Let me know if you have any tips about this. Great work nevertheless! :)

    • Basem,

      1. Controller Names: You are absolutely correct; areas, root level controllers and controllers within areas need to be unique, this is not a constraint of my design but the convention that we have to follow because of the MVC runtime, in how it maps the a Url request to an actual controller. A Url is unique therefore the controller it maps to also needs to be, or else it will not know which controller to execute, hence the exception you are getting.

      2. Plugin Routes: Yes the global.asax from the plugins are not used because really there can only be one global.asax.cs that can be used for an entire site. This was actually a great question so I added a section to the blog post “Registering Routes from Plugins” for you and everyone else (also re-uploaded the solution with this in it).

Comments are closed.