Twitter About Home

App Areas in ASP.NET MVC, take 2

So the discussion continues: How do you partition an ASP.NET MVC application into separate “areas” or “modules” (e.g., blog module, e-commerce module, forums module), then compose a finished app from those areas or modules?

Published Nov 5, 2008

Phil Haack, ASP.NET MVC program manager, just posted a prototype application structured in that way. It’s a really neat solution, and overcomes a number of issues that others, including myself, have experienced when trying to do it previously.

In this post, I’m going to take Phil’s prototype and tweak it in a few ways to my liking. That’s not to say that there’s anything wrong with his design, but only that I want to throw in some extra ideas that might make it even slicker in some cases.

How it works

The mechanism as given comes in two parts:

  • Routing configuration: There’s an extension method on RouteCollection called MapAreas() which lets you register a URL pattern for multiple areas. You pass it a URL pattern, a controller “root namespace”, and an array of area names. It prefixes “{area}/” to your URL pattern, and for each area name, it registers a route entry that targets controllers who live in the namespace given by:  <div align="center" style="margin-bottom:1em;"> (root namespace + “.Areas.” + area name + “.Controllers”) </div>

  • View engine: There’s a special view engine called AreaViewEngine that uses a built-in convention to look for view templates in an area-specific folder.

Don’t worry if you don’t understand this. If you download Phil’s prototype and try it, you’ll find it’s all straightforward enough.

It works very nicely, and it uses a clever trick with route defaults and constraints so that when you’re generating outbound URLs, you can link to controllers/actions in any area by specifying the area name in your Html.RouteLink() call, or you can link to controllers/actions within the same area using a normal Html.RouteLink() call that doesn’t specify any area name.

Suggestions for enhancement

I’m not going to touch the view engine part of the prototype at all. All I want to achieve is a configuration system that’s slightly more natural (for me) and a modified set of conventions and rules that are a bit more flexible. Here’s how I’d like a simple areas routing configuration to look:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "blogs/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "forums/{controller}/{action}", new { controller = "Home", action = "Index" })
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

… and here’s an example of a very slightly more complex configuration:

// Routing config for the blogs area
routes.CreateArea("blogs", "AreasDemo.Areas.Blogs.Controllers",
    routes.MapRoute(null, "SpecialUrlForPosts", new { controller = "Home", action = "Posts" }),
    routes.MapRoute(null, "blg/{controller}/{action}/{id}", new { action = "Index", controller = "Home", id = "" })
);
 
// Routing config for the forums area
routes.CreateArea("forums", "AreasDemo.Areas.Forums.Controllers",
    routes.MapRoute(null, "myforums/SecretAdminZone/{action}", new { controller = "Admin", action = "Index" }),
 
    // Equally possible to construct routes using "new Route()" syntax too
    new Route("myforums/{controller}/{action}", new MvcRouteHandler()) {
        Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index" })
    }
);
 
// Routing config for the root area
routes.CreateArea("root", "AreasDemo.Controllers",
    routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" })
);

The point to notice is that each area is configured independently of the others, which for me feels more natural than defining a single URL pattern that for some reason applies to all areas.

It turns out to be dead easy to make it work like this. All you need is this simple CreateArea() extension method:

public static void CreateArea(this RouteCollection routes, string areaName, string controllersNamespace, params Route[] routeEntries)
{
    foreach (var route in routeEntries)
    {
        if (route.Constraints == null) route.Constraints = new RouteValueDictionary();
        if (route.Defaults == null) route.Defaults = new RouteValueDictionary();
        if (route.DataTokens == null) route.DataTokens = new RouteValueDictionary();
 
        route.Constraints.Add("area", areaName);
        route.Defaults.Add("area", areaName);
        route.DataTokens.Add("namespaces", new string[] { controllersNamespace });
 
        if (!routes.Contains(route)) // To support "new Route()" in addition to "routes.MapRoute()"
            routes.Add(route);
    }
}

Apart from that, there’s no difference from the original prototype (well, you can delete the original prototype’s MapAreas() and MapRootArea() methods: they’re no longer used).

Benefits

How does this differ from the original prototype? What conventions have changed?

  • URL patterns are no longer forced to start with the area name (though they can if you want). Within a single area, you can have some URLs that start with the area name, and some that don’t.
  • In fact, URL patterns are now totally independent of area names, which means your area names can simply be internal code words for software modules, never seen by the public. If that’s what you want.
  • Controller namespaces are independent of area names, and they don’t even have to be constant within a single area. Pick your own convention and follow it.
  • You can  configure each area’s routes in a separate block of code, using a sweet DRY syntax, which for me feels more natural than having a single method call that registers routes across all areas.
  • You don’t need any special code or configuration for the “root” area – that’s just another area like the others, usually just with shorter URL patterns.
  • You can keep using the familiar routes.MapRoute() and new Route()* *ways of building route entries, merely wrapping up groups of them in CreateArea() calls. No significant new API.

Apart from this, it uses almost exactly the same mechanism as in the original prototype.

Drawbacks

If enforcing conventions is your thing, then you might not appreciate the extra flexibility that comes with these changes.

Also, when you do cross-area links, you have to be careful to specify a controller name and not just assume the default controller will be used. Otherwise, the generated URL might reuse the current request’s controller name, which might not even exist in the destination area. This is because of an obscure technicality in how URL generation works (it doesn’t affect Phil’s original design because of how he requires all URLs to start with an “{area}” segment).

This isn’t worth explaining in detail – all I’ll say is that the solution is simply to make sure your cross-area links always specify a controller name. Of course, you almost certainly should be doing that anyway, because it would be very weird to link just to an action name on a different area without being clear about which controller hosts that action.

Summary

ASP.NET MVC continues to impress me with its flexibility. If you want to structure your app in terms of “modules” or “areas”, it doesn’t take much code to enable it. Phil’s approach to areas is the neatest I’ve seen so far. Personally I like the tweaks I’ve suggested above, but it’s subjective and you can do things your own way.

READ NEXT

DDD7 talk: “ASP.NET MVC – Show me the code!”

Just a quick announcement for UK readers: if you’re going to DDD7, you might be interested in the following session I’m presenting.

Published Oct 22, 2008