Site Meter
 
 

Partitioning an ASP.NET MVC application into separate "Areas"

UPDATE: This technique is improved in a subsequent post

MonoRail (an alternative .NET MVC web app framework) has a concept of “application areas”, which is a way of grouping related controllers together. For example, you might have a “Blog” area (including a bunch of controller classes related to blogging) and a “Calendar” area (another bunch of related controllers). It’s a nice way of splitting a huge project into a smaller set of manageable ones.

If we had this feature in ASP.NET MVC, we might put controller classes into namespaces that represent area, e.g.:

  • MyApp.Controllers.Blog.HomeController
  • MyApp.Controllers.Blog.AdminController
  • MyApp.Controllers.Calendar.HomeController
  • MyApp.Controllers.Calendar.AdminController
  • MyApp.Controllers.Calendar.RssController

… and also put view templates into corresponding subfolders, e.g.:

  • /Views/Blog/Home/Index.aspx
  • /Views/Blog/Home/Post.aspx
  • /Views/Blog/Admin/RecentComments.aspx
  • /Views/Calendar/Home/Index.aspx
  • /Views/Calendar/Home/ByMonth.aspx
  • /Views/Calendar/Admin/Permissions.aspx

… etc. However, as of Preview 4, ASP.NET MVC doesn’t yet have any such concept of “areas”. Fortunately, the MVC framework is very well designed for extensibility. So, how far can we get with implementing our own notion of “areas”?

A naive solution

Since preview 4, it’s been possible to have multiple controllers with the same name, as long as they’re in different namespaces. To tell the routing system which one to use for a given request, you can put a “namespaces” data token on each route entry, e.g.:

routes.Add(new NamespaceAwareRoute("blog/{controller}/{action}/{id}", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
    DataTokens = new RouteValueDictionary(new { namespaces = new[] { "MyApp.Controllers.Blog" } })
});
 
routes.Add(new NamespaceAwareRoute("calendar/{controller}/{action}/{id}", new MvcRouteHandler())
{
    Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
    DataTokens = new RouteValueDictionary(new { namespaces = new[] { "MyApp.Controllers.Calendar" } })
});

Great! So now, requests for /Blog/* will go to the controllers in MyApp.Controllers.Blog, while requests for /Calendar/* will go to the controllers in MyApp.Controllers.Calendar. How very lovely. We’ll get to the problem with this in a moment.

Putting view templates into subfolders per namespace

Next, to implement the namespaced view folders convention (i.e. finding views at /Views/(Namespace)/Controller/ViewName.aspx), we can implement a custom IViewLocator. To do this, how about an [ActionFilter] attribute that detects when you’re about to render a view, and jumps in to alter the set of directories that are searched for the view template?

public class GetViewsFromNamespaceFolderAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        ViewResult result = filterContext.Result as ViewResult;
        Controller controller = filterContext.Controller as Controller;
        if ((result != null) && (controller != null))
        {
            WebFormViewEngine viewEngine = controller.ViewEngine as WebFormViewEngine;
            if (viewEngine != null)
                viewEngine.ViewLocator = new NamespaceAwareViewLocator(GetLastTokenFromControllerNamespace(controller));
        }
    }
 
    private string GetLastTokenFromControllerNamespace(Controller controller)
    {
        string[] tokens = controller.GetType().FullName.Split('.');
        if (tokens.Length > 1)
            return tokens[tokens.Length - 2];
        else
            return "";
    }
 
    public class NamespaceAwareViewLocator : ViewLocator
    {
        public NamespaceAwareViewLocator(string ns)
        {
            ViewLocationFormats = new[] {
                "~/Views/"+ns+"/{1}/{0}.aspx", "~/Views/"+ns+"/{1}/{0}.ascx",
                "~/Views/"+ns+"/Shared/{0}.aspx", "~/Views/"+ns+"/Shared/{0}.ascx",
                "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx"
            };
            MasterLocationFormats = new[] {
                "~/Views/"+ns+"/{1}/{0}.master",
                "~/Views/"+ns+"/Shared/{0}.master",
                "~/Views/Shared/{0}.master"
            };
        }
    }
}

With this defined, you just need to tag your controller class with the attribute,

namespace MyApp.Controllers.Calendar
{
    [GetViewsFromNamespaceFolder]
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            return View();
        }
    }
}

… and it will indeed find its view templates in /Views/Calendar/Home. So, we’re done, right?

Not so fast

The problem with having multiple controllers with the same name is that, even though ASP.NET MVC understands it, the core routing system does not. System.Web.Routing has no awareness of the “namespaces” DataToken value, so when it comes to generating outbound URLs, namespaces are simply ignored.

In other words, if you generate a URL with <%= Html.ActionLink(“Click me”, “MyAction”, “Home” ) %>, or <%= Url.Action(“MyAction”) %>, or return RedirectToAction(…),  the routing system pays no heed to namespaces, either those defined in the routing configuration, or the namespace of the controller you’re currently on, and it just returns the first URL that matches the given controller/action name. There’s no way to tell it to try to keep on the ‘active’ namespace, nor even to make it filter the set of acceptable route entries by namespace.

A few of us had a discussion about this on the ASP.NET MVC forums, and the consensus was “you’ll just have to use named routes, all the time.” Personally, I think that would be pretty lame. Giving names to routes is a bit of a crutch really, it’s not far from being a dirty hack – one of the key selling points of System.Web.Routing is that it should let you decouple the concern of routing from the concern of creating links and redirections. Named routes mangle these concerns together. (If you personally prefer to give names to your routes, (“Fluffy”, “Snowy”, and “Bunnykins” are nice names) then don’t take offence – you can do whatever you like.)

Solving the outbound routing problem

So, what’s to be done? One option is to create a custom Route subclass that’s aware of DataTokens["namespaces"], and somehow gives priority to Route entries that match the namespace of the “current” request. That’s pretty tricky: you can only control the behavior of one Route entry at a time – you can’t control them as a group – so it’s hard to make them co-operate to change the normal rules of priority. However, if you’re willing to write some slightly odd-looking code, you can do it.

The idea with NamespaceAwareRoute is to pre-scan the RouteTable.Routes collection to try to find a matching route that also matches the namespace of the current request. If we find one, use that as the result. If not, revert to the usual behavior for Route entries, setting some flag that says “don’t scan any more”. Watch out, this is quite a big hunk of code:

private class NamespaceAwareRoute : Route
{
    const string _scanKey = "__NamespaceAwareRouteHasScanned";
 
    public NamespaceAwareRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler) { }
    public NamespaceAwareRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler) { }
    public NamespaceAwareRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler) { }
    public NamespaceAwareRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler) { }
 
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        string controller = GetCurrentControllerTypeName(requestContext);
        if ((controller != null) && !RouteMatchesControllerNamespace(this, controller))
        {
            // If we haven't already done a scan for these values, do one now
            if (!requestContext.HttpContext.Items.Contains(_scanKey + values.GetHashCode()))
            {
                var vpd = GetVirtualPathWhereNamespaceMatches(requestContext, values, controller);
                if (vpd != null)
                    return vpd;
                requestContext.HttpContext.Items[_scanKey + values.GetHashCode()] = "scanned";
            }
        }
 
        return base.GetVirtualPath(requestContext, values);
    }
 
    private VirtualPathData GetVirtualPathWhereNamespaceMatches(RequestContext requestContext, RouteValueDictionary values, string controller)
    {
        using (RouteTable.Routes.GetReadLock())
        {
            foreach (Route route in RouteTable.Routes
                                              .OfType<route>()
                                              .Where(r => RouteMatchesControllerNamespace(r, controller)))
            {
                var vpd = route.GetVirtualPath(requestContext, values);
                if(vpd != null)
                    return vpd;
            }
        }
        return null;
    }
 
    private string GetCurrentControllerTypeName(RequestContext ctx)
    {
        var controllerContext = ctx as ControllerContext;
        if (controllerContext == null)
            return null;
        else
            return controllerContext.Controller.GetType().FullName;
    }
    private bool RouteMatchesControllerNamespace(Route route, string controllerTypeName)
    {
        if (route.DataTokens != null)
        {
            var namespaces = route.DataTokens["namespaces"] as IEnumerable<string>;
            if (namespaces != null)
                foreach (string ns in namespaces)
                    if (controllerTypeName.StartsWith(ns + "."))
                        return true;
        }
        return false;
    }
}

It’s easy to use NamespaceAwareRoute. Just replace your Route objects with NamespaceAwareRoute objects. It doesn’t change the API in any way. (Unfortunately you can’t use routes.MapRoute(…) with this, because that’s hard-coded to add Route objects.) Other than that, you’ll find that outbound URL generation starts to prefer route entries that match the namespace of the “current” controller (when we can identify it).

So, what do you think? Is this actually useful? Would you really want to partition your app into “areas” like this? Are there any more “gotchas” I haven’t considered? Note that this code is proof-of-concept, not polished and load-tested. Use it at your own risk.

17 Responses to Partitioning an ASP.NET MVC application into separate "Areas"

  1. Thanks for sharing this! It’s very useful for administration areas where almost all controller names are duplicated.

  2. @Petr: Sometimes you can get around that by securing the admin actions of a controller…

    like User/show/5 should be public
    but User/edit/5 should be protected…

    I can see that you might want a different controller for administrators in more complex scenarios though.

    @Steve: Interesting that you posted this today. Last night I posted something similar (http:/flux88.com/ARoutingEvolution.aspx), but I solved the issue with routing tricks, and I still cannot have duplicate named controllers.

    While I agree that there is a need for Areas, I think that the team should take this feedback and make the routing system a bit more extensible. Writing that much code to make something like this work is really absurd if the framework is supposed to tout extensibility.

    Anyway, I’m glad to see you got it working. Hopefully we can eventually use this approach with the standard Route class.

  3. Pingback: Routes and namespaces - LA.NET [EN]

  4. Steve

    @Ben: Yes, requiring this much code is a bit of a mess! (Unless anyone else has a more concise solution?) I totally agree that the routing system could use a bit more extensibility. Specifically, some way to override RouteTable.Routes.GetVirtualPath() – then you could quite easily implement a different priority order without resorting to such a weird algorithm.

    With this post, I wasn’t trying to say “this is how you should structure your MVC apps”, but rather “*if* you wanted MonoRail-style ‘areas’, what would it take?” Personally I’m not convinced that many people need more than a handful of controller classes, even for a moderately sized application, so I find it unclear that ‘areas’ really are a necessary feature.

    However, Preview 4′s treatment of namespaces seems a bit half-baked: it suggests you should be able to have multiple controllers with the same name, and it works fine with inbound routing, but fails with outbound routing. If you’re not supposed to use it this way, then maybe they should remove the per-Route “namespaces” DataToken feature, and just have a global static list of namespaces for DefaultControllerFactory to search regardless of the chosen Route entry.

  5. Mike

    Great post, thanks! I’d love to have ‘areas’, in fact I think I _need_ them. I do worry thought that because System.Web.Routing is already released, Microsoft can’t actually add this feature, is that right?

    So we would need your solution, which you could put on Codeplex if enough people are interested.

    Thanks!

  6. Steve

    When I said “the current routing system could use a bit more extensibility, specifically the facility to override RouteTable.Routes.GetVirtualPath()…” – I take it back! With further consideration, it turns out to be quite straightforward. http://blog.codeville.net/2008/07/31/implementing-a-custom-route-priority-order/

  7. Pingback: Reflective Perspective - Chris Alcock » The Morning Brew #149

  8. Pingback: HowTo: ASP.NET MVC erstellen (erster Einstieg) | Code-Inside Blog

  9. How is giving names to routes a crutch or a dirty hack. These names are completely abstract. In your implementation, just think of them as “areaName” instead of “routeName” and you’re all set.

  10. Another approach is to add “area” (along with a constraint) as a URL parameter to your route. This would require a custom route though since you’d have to map the “area” with a namespace somehow.

    Route URL: “{area}/{controller}/{action}/{id}”
    Constraint: new {area=”Blog|Admin|…”}

    Thus when you generate a URL, you wouldn’t need to use named routes. You’d do this:

    Url.Action(new {area=”blog”, id=”123″}

    But as you can see, that’s really not any different conceptually than just using named routes.

  11. Steve

    @Haacked (#9) – you can’t just use routeName as an “area name”, because System.Web.Routing forces all route names to be unique. The idea with areas is that they represent a whole module within the application, so might well have multiple URL patterns and hence multiple route entries.

    The reason I prefer nameless routes, even though route names are completely abstract, is that named routes force the developer to be aware of the current routing configuration every time they render a link or perform a redirection. The programmer can’t simply specify which controller/action she wishes to reach, but instead has to be concerned about what arbitrary name that currently corresponds to. This undermines the separation of concerns between placing links/redirections and designing a URL schema. It also makes changing your routing config quite dangerous.

    @Haacked (#10) – yes, that’s a good technique. I think “tgmdbm” suggested something like that on the ASP.NET MVC forums a while back. The downside to that approach is that your routing config has to be quite funky-looking – each entry specifies an {area} parameter, a Constraint that prevents {area} from really being a parameter, and a list of namespaces. The main benefit of my technique is that you don’t need the weird param/constraint pair.

    However, in retrospect, I prefer your technique over mine because it involves less surgery on the low-level routing process.

    I think the param/constraint technique *is* conceptually different to using named routes, because {area} is basically a tag for a whole segment or module within the application consisting of multiple independent controllers, probably all built at a different time during the app’s lifetime by a different programmer. Named routes, on the other hand, identify a single exact route entry, which is far harder to manage as your app scales up in complexity.

  12. Hey Steve, I just blogged a prototype that I think addresses most of these concerns. http://haacked.com/archive/2008/11/04/areas-in-aspnetmvc.aspx

  13. Pingback: App Areas in ASP.NET MVC, take 2 « Steve Sanderson’s blog

  14. Steve

    @Haacked – thanks for letting me know. Your design looks like it works well, certainly better than the one presented in this blog post. I’ve done a new post about it at http://blog.codeville.net/2008/11/05/app-areas-in-aspnet-mvc-take-2/

  15. Pingback: HowTo: First steps with ASP.NET MVC | Code-Inside Blog International

  16. You need to look into some of the research conducted during the Third Reich. ,

  17. Ngoc Manh Nguyen

    Please send me the sample code!
    Thanks a lot!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">