Using controllers with the same name in ASP.NET MVC

Introduction

Let's consider the following ASP.NET MVC structure:

MvcSite
└ Controllers
    ├ HomeController
    └ NewsletterController
└ Models
    └ NewsletterModel
└ Views
    └ Home
        └ Index
    └ Newsletter
        └ Index

There's just a homepage, where the view includes a newsletter box rendered as a partial view by the newsletter controller. The newsletter box shows an e-mail and a label asking the user to subscribe to the newsletter.

Imagine that we want to extend the newsletter box functionality, allowing the user to type his name too. However, we don't want to modify the original controller. A possible use case for this is where you have access to the original code but you don't want to modify it, e.g. the code is from an open source application and you want to be able to update to new versions easier (well, it's never gonna be that easy, but ok).

We end up with the following scheme:

MvcSite
└ Controllers
    ├ HomeController
    └ NewsletterController
└ Models
    └ NewsletterModel
└ Views
    └ Home
        └ Index
    └ Newsletter
        └ Index
└ Special
    └ Controllers
        └ NewsletterController
    └ Models
        └ NewsletterModel
    └ Views
        └ Newsletter
            └ Index

There's a new folder, Special, that contains the familiar MVC structure. However, it only contains what we want to override.

Notice that there are now two controllers by the same name, NewsletterController. They are in the expected namespaces, MvcSite.Controlers and MvcSite.Special.Controllers. However, in ASP.NET MVC the significant identifier of a controller is just its name, not the full type name. Therefore, we have an ambiguity problem.

Routing

To fix this, we're going to modify the default route:

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

by specifying the namespaces it should use:

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

The controllers we haven't overriden will still be picked up as fallback, so the home page, served by the HomeController will still work.

A different solution to the ambiguity problem is to implement an additional route that maps only the overriden controllers. This can be a route identical to the default, but with an extra constraint on the controller parameter. The constraint will check at runtime with reflection if there's a controller class under MvcSite.Special.Controllers.

View selection

Even though we're now using the correct controller, the view is still the old one. That's because the controller's action looks like this:

public override ActionResult Index()
{
    return PartialView("Index", new NewsletterModel { Reason = "best newsletter ever!" });
}

The default view engine is searching still on a path like Views/Controller/Action and the controller is still identified as Newsletter.

One way of solving it is by specifying an explicit file path in the controller action:

public override ActionResult Index()
{
    return PartialView("~/Special/Views/Newsletter/Index.cshtml", new NewsletterModel { Reason = "best newsletter ever!" });
}

But, arguably, this looks a bit ugly.

We can solve this in a different way, by overriding the default view engine in Global.asax:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());

and the implementation of the CustomViewEngine:

public class CustomViewEngine : RazorViewEngine
{
    public CustomViewEngine()
    {
        ViewLocationFormats = new[]
            {
                "~/Special/Views/{1}/{0}.cshtml"
            }.Union(ViewLocationFormats).ToArray();

        PartialViewLocationFormats = new[]
            {
                "~/Special/Views/{1}/{0}.cshtml"
            }.Union(PartialViewLocationFormats).ToArray();
    }
}

So we're using a new view engine, based on the default RazorViewEngine, that will first check for view files into our Special/Views subfolder before diving into the default folders.

Note that you'll also need to copy the web.config of the regular Views folder into the Special/Views folder, otherwise compilation of the views won't work.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s