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?
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.
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.
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
Create a separate module as shown 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");