« Back to home

Overriding MVC's DefaultControllerFactory

A little while ago I worked on a project that uses Nopcommerce. In this case I did require me to override some of the Controllers. So I went of and defined some Controller is my own namespace. Only to then run into trouble with the DefaultControllerFactory:
System.InvalidOperationException: Multiple types were found that match the controller named 'User'. Now why does this happen? Let's start with a little drawing of the situation. alt

The Framework contains two assemblies that have Controllers, one set for the public site and another for the Admin area. The third assembly are Framework customisations. The HomeController hase three implementations: The framework default, a customised one to use instead of the framework default and one in the Admin area. The User controller has two implementations, one in public and on in the Admin area. The default route is straight forward.

routes.MapRoute(  
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    new[] { "CustomControllerFactory.Controllers" }
);

The framework also registers the admin area:

context.MapRoute(  
    "Admin_default",
    "Admin/{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", area = "Admin", id = "" },
    new[] { "FrameworkAdmin.Controllers" }
);

The problem

When user/index is requested MVC's DefaultControllerFactory will look for a class UserController that implements the action Index. It does so by searching the default namespace, which is set to CustomControllerFactory.Controllers in the default route. When not found in will try to find it in all loaded assemblies. Then it finds two, one in the public area and the other in the admin area. This results in a System.InvalidOperationException.

Solution

Luckily MVC allows the DefaultControllerFactory to be overridden. Simply add the following line to the Application_Start method.

ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));  

Where CustomControllerFactory is the name of the custom implementation.

The logic needed is simple: If a controller is not implemented in the custom namespace and this is not the Admin area, then get the Contoller from the Framework namespace. This can be achieved by overriding GetControllerType:

protected override Type GetControllerType(RequestContext requestContext, string controllerName)  
{
    var list = ControllerScanner.Instance.Controllers;
    if (!list.Contains(controllerName) && !"Admin".Equals(requestContext.RouteData.DataTokens["area"]))
    {
        requestContext.RouteData.DataTokens["namespaces"] = new[] { "Framework.Controllers" };
    }
    var controllerType = base.GetControllerType(requestContext, controllerName);
    return base.GetControllerType(requestContext, controllerName);
}

ControllerScanner is a class which scans the current assembly for Controllers. The core logic is a single LINQ query:

_controllers = Assembly.GetExecutingAssembly().GetTypes()  
    .Where(type => typeof(Controller).IsAssignableFrom(type))
    .Select(t => t.Name.Remove(t.Name.IndexOf("Controller")))
    .ToArray();

For performance reasons you should make sure the assembly is only scanned once. To keep this example small and simple, I used an excellent Singleton implementation from this article. Normally, however, I avoid Singleton implementations and have the Dependency Injection take care of instantiation.

Full example code on GitHub