30

My goal is to modify asp.net mvc's controller registery so that I can create controllers and views in a separate (child) assembly, and just copy the View files and the DLLs to the host MVC application and the new controllers are effectively "Plugged In" to the host app.

Obviously, I will need some sort of IoC pattern, but I'm at a loss.

My thought was to have a child assembly with system.web.mvc referenced and then to just start building controller classes that inherited from Controller:

Separate Assembly:

using System.Web;
using System.Web.Mvc;

namespace ChildApp
{
    public class ChildController : Controller
    {
        ActionResult Index()
        {
            return View();
        }
    }
}

Yay all fine and dandy. But then I looked into modifying the host application's Controller registry to load my new child controller at runtime, and I got confused. Perhaps because I need a deeper understanding of C#.

Anyway, I thought I needed to create a CustomControllerFactory class. So I started writing a class that would override the GetControllerInstance() method. As I was typing, intellisence popped this up:

Host MVC Application:

public class CustomControllerFactory : DefaultControllerFactory 
{
    protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
    {
        return base.GetControllerInstance(requestContext, controllerType);
    }
}

Now, at this point I'm at a loss. I don't know what that does. Originally, I was going to write a "Controller Loader" class like a Service Locator like this:

Host MVC Application:

public class ControllerLoader
{
    public static IList<IController> Load(string folder)
    {
        IList<IController> controllers = new List<IController>();

        // Get files in folder
        string[] files = Directory.GetFiles(folder, "*.plug.dll");
        foreach(string file in files)
        {
            Assembly assembly = Assembly.LoadFile(file);
            var types = assembly.GetExportedTypes();
            foreach (Type type in types)
            {
                if (type.GetInterfaces().Contains(typeof(IController)))
                {
                    object instance = Activator.CreateInstance(type);
                    controllers.Add(instance as IController);
                }
            }
        }
        return controllers;
    }
}

And then I was planning on using my list of controllers to register them in the controller factory. But ... how? I feel like I'm on the edge of figuring it out. I guess it all bois down to this question: How do I hook into return base.GetControllerInstance(requestContext, controllerType);? Or, should I use a different approach altogether?

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
quakkels
  • 11,676
  • 24
  • 92
  • 149

4 Answers4

29

Reference the other assembly from the 'root' ASP.NET MVC project. If the controllers are in another namespace, you'll need to modify the routes in global.asax, like this:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        namespaces: new[] { typeof(HomeController).Namespace }
    );

}

Notice the additional namespaces argument to the MapRoute method.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 3
    That's the thing though. I'm trying to get this to work without recompiling the Root/Host application for each new Controller Assembly. – quakkels Sep 26 '11 at 19:23
  • Wait... waiiiiit. Maybe this works without a reference! I'm trying `namespaces: new[] {typeof(IController).Namespace}`. We'll see if that works without adding a reference from HostApp to ChildApp. – quakkels Sep 26 '11 at 19:40
  • 1
    @quakkels ARGH you are killing us with the suspense... I'm tired of waiiiiit, did it work?!? – Joel Peltonen Mar 19 '14 at 13:50
  • @Nenotlep - What do you mean? I marked it as the answer. – quakkels Mar 19 '14 at 22:03
  • This was a few years ago. Here's a more current version of the same question: http://stackoverflow.com/questions/22176425/how-to-add-new-dll-locations-to-webapis-controller-discovery – quakkels Mar 19 '14 at 22:15
  • @quakkels I mean did `typeof(IController).Namespace` work or did you go with Marks suggestion and hardwire HomeController into the route? – Joel Peltonen Mar 20 '14 at 09:02
  • Unfortunately, I do not have access to the original source anymore. This was using either asp.net mvc 2 or mvc 3. Perhaps you could try the techniques outlined here and make your decision from your results. – quakkels Mar 20 '14 at 20:20
3

Your other project should be set up like an area and you will get the desired result. The area registration class will bring your area project into the mix. Then you can just drop the dll into a running app and it will run without building the entire app and redeploying it.

The easiest way to do it is to add a new mvc project to your solution and have Visual Studio create the new project inside /areas/mynewprog/. Delete out all the files you don't need and add an area registration class to wire it up.

Build the project, grab it's dll and drop it into your running websites bin directory and that its..

You then just have to deal with the views and stuff, either compile them into the dll or copy them to the server into the areas/mynewproj/ folder.

JBeckton
  • 7,095
  • 13
  • 51
  • 71
  • Do you happen to have a more verbose example or tutorial about this? I would really like to investigate this method further as it seems so elegant and dynamic. Do the compiled views work with Razor? So that the Razor layout is in the host app and the view itself is within the DLL? – Joel Peltonen Mar 19 '14 at 13:48
1

Create a separate module as shown Plugin Project structure:

Plugin Project structure

Notice that some files where excluded from the project.

The MessagingController.cs file is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MessagingModule.Controllers
{
    public class MessagingController : Controller
    {
        //
        // GET: /Messaging/

        public ActionResult Index()
        {
          var appName = Session["appName"]; // Get some value from another module
            return Content("Yep Messaging module @"+ appName);
        }

    }
}

The MessagingAreaRegistration file is as shown:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace MessagingModule
{
  public class MessagingAreaRegistration : AreaRegistration
  {
    public override string AreaName
    {
      get
      {
        return "Messaging";
      }
    }

    public override void RegisterArea( AreaRegistrationContext context )
    {
      context.MapRoute(
          "Messaging_default",
          "Messaging/{controller}/{action}/{id}",
          new { action = "Index", id = UrlParameter.Optional }
        //new[] { "MessagingModule.Controllers" }
      );
    }

  }
}

The relevant potion of the Global.asax.cs file is as follows:

[Load the external plugin (controllers, api controllers library) into the current app domain. After this asp.net mvc does the rest for you (area registration and controllers discovery ][3]

   using System;
   using System.Collections.Generic;
   using System.Linq;
   using System.Web;
   using System.Web.Http;
   using System.Web.Mvc;
   using System.Web.Optimization;
   using System.Web.Routing;
   using System.Reflection;
   using System.Web.Configuration;
   using System.Text.RegularExpressions;

   namespace mvcapp
   {
     // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
     // visit http://go.microsoft.com/?LinkId=9394801

     public class MvcApplication : System.Web.HttpApplication
     {
       protected void Application_Start()
       {
         var domain = AppDomain.CurrentDomain;

         domain.Load("MessagingModule, Version=1.0.0.0, Culture=neutral,   PublicKeyToken=null");
tomRedox
  • 28,092
  • 24
  • 117
  • 154
RorvikSam
  • 11
  • 2
0

my experience with controllers is that this just works. Copy the assembly into the bin directory (or add a reference to the assembly and it will be copied there for you)

I have not tried with views

pm100
  • 48,078
  • 23
  • 82
  • 145
  • Would that work even if the two projects are not referenced to each other. I'm trying to make it so I can add controllers to a live website without recompiling the host application. – quakkels Sep 26 '11 at 19:14
  • Sorry, no luck. I copied the ChildApp DLL to the HostApp bin with no luck. – quakkels Sep 26 '11 at 19:28
  • are u sure. I have the same aim and could swear that all it took was a restart (thats still too much though) – pm100 Sep 27 '11 at 21:16
  • 1
    Were your namespaces the same? I imagine that then it would work, but if ChildController is in ChildApp namespace and the host project is in the HostApp namespace it might not work. Can you confirm? – Joel Peltonen Mar 21 '14 at 11:08