App Areas in ASP.NET MVC, take 2
ASP.NET, MVC, Routing November 5th, 2008So 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?
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:
(root namespace + “.Areas.” + area name + “.Controllers”)
- 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.


November 5th, 2008 at 3:16 pm
Another great post, really looking forward to the book coming out because if it’s anything like these blog entries it’ll be an invaluable MVC resource.
November 5th, 2008 at 4:09 pm
I like them as well, and it’s easy enough to have versions of MapArea that are more convention based that wrap the versions you wrote.
For my prototype, I went a bit overboard with the conventions because it made developing the prototype faster. As I take it further, I’ll probably dial it back a bit to something more manageable like what you did. Nice work!
November 5th, 2008 at 5:07 pm
Great post. Just what I was looking for. Can you please post the sample code?
November 5th, 2008 at 6:20 pm
Are things like this going to make it into your upcoming book are are they too last minute?
November 6th, 2008 at 8:34 am
[…] projects and merged into one large master application. Also check out Steve Sanderson’s follow-up post with more ideas on this […]
November 6th, 2008 at 1:35 pm
@Maciej - to get this working, download Phil Haack’s prototype (linked from http://haacked.com/archive/2008/11/04/areas-in-aspnetmvc.aspx), then copy & paste this blog post’s CreateArea() extension method listing into it. Then you can change the routing configuration to look like one of the listings I posted above.
@Eric - it’s not completely certain what will be in my upcoming book when it’s finished, because that will depend on how the MVC framework itself looks at RTM. The book takes developers from a low knowledge of ASP.NET MVC to a thorough and in some places advanced knowledge, focusing on how the framework is intended to be best used, whereas my blog tends to emphasise more experimental techniques such as “app areas” and “partial requests”.
November 6th, 2008 at 2:13 pm
[…] App Areas in ASP.NET MVC, Take 2 (Steve Sanderson) […]
November 8th, 2008 at 12:22 am
Thanks Steve.
Hope you include enough Advanced stuff in the book!
November 8th, 2008 at 9:47 pm
Thanks for this post - it makes Phil’s idea more practical & natural.
I would like to use this approach to my next application just coming.
November 18th, 2008 at 2:52 pm
Thanks! This works great. Now I have one question about the web.config file. Each one of our “areas” needs to have specific information that would not be known at the top level. i have added a web.config file to each area but cannot seem to get the appsettings section to load into the Configuration object.
November 19th, 2008 at 5:34 pm
@Eric - you might like to create a separate web.config file holding settings for each area, then reference all those files in your top-level web.config. For an example, see http://www.devx.com/vb2themax/Tip/18880 . Note that you can use the “file” attribute to import arbitrary config nodes, not just appSettings.
December 14th, 2008 at 1:56 pm
[…] (Here is another take on this approach by Steve Sanderson) […]
December 22nd, 2008 at 8:45 pm
What can you see as being the cleanest method of preventing the routing mechanism ‘falling through’ to the root controllers, etc?
For example, if you have a mobile and main website, some sections will be deliberately left out of the cut-down mobile site.
It would be useful to be able to return a 404 if one of these is requested, instead of falling through.
December 23rd, 2008 at 8:40 am
@Rich - You might want to put all your “mobile” routes at the top of the config, followed by an entry that catches all mobile requests and returns a 404, followed by all the regular route entries. Exactly how to do that depends on how you are distinguishing “mobile” URLs or requests from regular ones.
December 23rd, 2008 at 9:38 am
Hi,
I am using asp.net mvc preview 5 for one of my project. If i run my website from IIS 5 then it throws below error. But if i run using visual studio 2008 F5 then it works fine. If anyone faced this kind of problem please let me know how you fixed it
Regards,
Saravanan
December 23rd, 2008 at 11:23 am
@Steve: thanks for the rapid reply - I’ll give it a go.
January 18th, 2009 at 8:27 pm
This has been extremely helpful, looking forward to the book, just running into one problem…
On XP Pro, VS 2008 SP1 and MVC Beta.
Put “Manager” area in own folder and can access the controllers with full Url, however it’s not hitting the default controller.
// Routing config for the manager area
routes.CreateArea(”Manager”, “AreasDemo.Manage.Controllers”,
routes.MapRoute(null, “manage/{controller}/{action}”,
new { controller = “Category”, action = “List” })
);
http://localhost:2088/home WORKS
http://localhost:2088/manage/category WORKS
http://localhost:2088/manage/category/list WORKS
BUT…
http://localhost:2088/manage FAILS (HTTP 404)
January 19th, 2009 at 9:31 am
@Matthew - there must be some issue to do with your routing config, but I can’t tell what it is without seeing more. If you can email me a minimal runnable ASP.NET MVC project that demonstrates the issue then I’ll have a look.
January 23rd, 2009 at 12:59 am
[…] of it as needed. For example, for S#arp Architecture, I took guidance from Phil Haack and Steve Sanderson but modified the merged result to support areas directly under the Views folder. (I […]
February 14th, 2009 at 10:06 pm
This has been extremely useful, thanks! I was trying to get Phil’s solution to work with the route “/site/{*id}” but kept getting errors when it tried to find the relevant view. After creating the area using your method, it worked instantly! Thanks again.
February 16th, 2009 at 10:30 am
@Andrew - Great! Glad to be of help.
February 19th, 2009 at 10:28 am
[…] Grouping Controllers with ASP.NET MVC: Phil Haack的又一篇精彩贴子,讨论了如何将一个ASP.NET MVC应用分成多个“区域(areas)”或“模块(modules)”,这些区域或模块可以在单独的项目中开发,然后合并成一个大的主应用。也参阅Steve Sanderson在这个题目上提供了更多想法的跟贴。 […]
February 23rd, 2009 at 1:08 pm
Using it, loving it!
Thanks so much for doing this, it really solved some issues I had with multiple projects nested in IIS… I don’t even want to go there anymore now that I have areas it is much more simple.
March 20th, 2009 at 4:57 am
I’m having the same problem as Matthew. If I explicitly go to a URL within an area then everything is fine, but it never seems to find the default route for an area.
This works great:
http://www.domain.com/Client/Home/Index
This gives a 404 error:
http://www.domain.com/Client
Any ideas?
April 26th, 2009 at 7:21 pm
[…] the marketplace. In fact, many of the extension are in completely unsupported places. Steve Sanderson talks about a mechanism for areas. CodeCampServer has some smart input builders. MvcContrib has lots of goodies. […]
May 1st, 2009 at 8:42 pm
[…] Phil Haack’s idea from this post and reuses Steve Sanderson’s helpers presented in this second post. They do an excellent work in describing this feature and since there’s really not much I can add […]
May 13th, 2009 at 9:39 pm
I ran into a problem with using AutoFac and this code where controllers had to have unique names. I figured out how to extend the area capability into the ControllerFactory by implementing an identification strategy and subclassing the factory. Check it out at http://blogs.thirdfamily.com/michael/?p=37
June 3rd, 2009 at 4:37 am
[…] the same data but in vastly different ways. A quick google search led me to posts by Phil Haack and Steve Sanderson, and the resulting AreaViewEngine class derived from their code worked well, with one major issue: […]
June 3rd, 2009 at 9:41 pm
[…] Phil Haacked showed a concept called area’s to partition an ASP.NET MVC application. After that, Steve Sanderson came up with an improved version of the […]
July 7th, 2009 at 1:56 pm
What about canonical URL’s generated by a SiteMap and parameters passed to a URL using app areas? For example, check out the canonical URL section in http://www.asp.net/learn/mvc/tutorial-20-cs.aspx. It only defines a solution per URL using MapRoot. So generating links via a SiteMap isn’t do-able the minute you pass parameters to the URL as it hasn’t got any URL to match up with.
July 8th, 2009 at 7:45 am
@Kezzer - I’m not sure what problem you’d experience. Are you suggesting that you’re generating your routing config from the sitemap, and therefore can’t accept arbitrary querystring parameters in URLs?
If that’s the case, you might want to check out Chapter 15 of my ASP.NET MVC book which describes a way of working with SiteMaps that’s fully compatible with ASP.NET MVC-style routing with arbitrary parameters.
If I’ve misunderstood your point, please let me know!
August 16th, 2009 at 9:59 pm
@Steve,
I’m using these app areas and they’re great. I’m now trying to introduce handling of the odd legacy url, using what seems to be the most widely used code by Matt Hawley:
http://blog.eworldui.net/post/2008/04/ASPNET-MVC—Legacy-Url-Routing.aspx
the trouble is, I can’t get it to work - I’ve tried adding my LegacyRoutes both inside and outside the routes.CreateArea in Global.asax.cs but no joy.
Are you familiar with this code and if so, any idea how to get it to work with your app areas? Might be a good blog post!
Cheers,
Rich
November 16th, 2009 at 10:43 pm
Phil’s blog http://haacked.com/archive/2009/07/31/single-project-areas.aspx
is dated after this one and you guys are refining it constantly. I have been asked to decipher and introduce areas in our project and I’m a newbie in web development. I’m not sure which approach I must follow for introducing areas.
Finally Steve, I bought your book published in 2009 and I didnt find any discussion on design by areas.
November 26th, 2009 at 8:54 pm
This is for anyone who is new to areas. This solution is great when incorporated in Phil’s prototype. However, both failed to work for me and I think because I’m using MVC preview-2. I changed references in Phil’s protiotype to System.Web.Mvc 2.0.0.0 and it stoppped working.
So please make sure what ver. of assembly you are working with.