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?
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.
38 Responses to App Areas in ASP.NET MVC, take 2
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.
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!
Great post. Just what I was looking for. Can you please post the sample code?
Are things like this going to make it into your upcoming book are are they too last minute?
Pingback: Nov 6th Links: ASP.NET, ASP.NET AJAX, jQuery, ASP.NET MVC, Silverlight and WPF - ScottGu's Blog
@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”.
Pingback: Dew Drop - November 6, 2008 | Alvin Ashcraft's Morning Dew
Thanks Steve.
Hope you include enough Advanced stuff in the book!
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.
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.
@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.
Pingback: Areas in ASP.NET at { null != Steve }
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.
@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.
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
@Steve: thanks for the rapid reply – I’ll give it a go.
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)
@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.
Pingback: MVC "Areas" as Hierarchical Subfolders under Views - Billy McCafferty
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.
@Andrew – Great! Glad to be of help.
Pingback: 15、ASP.NET MVC Beta 发布了 - Sample Weblog - IC窗口
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.
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?
Pingback: You should NOT use ASP.NET MVC if. . . : Jeffrey Palermo (.com)
Pingback: The S#arp framework: wrapping up the SharpArch.Web assembly study - LA.NET [EN]
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
Pingback: Areas in ASP.NET MVC « Memory Dump
Pingback: Martijn Boland » ASP.NET WebForms and MVC together in one project
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.
@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!
@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
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.
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.
Routing configuration is perhaps the most infuriating aspect I’ve been faced with since trying to use Areas.
Everyone I’ve spoken to appears to have one code sample or the other but don’t really understand how to knit routing when faced with a combination of:
- Areas
- unlimited depth SEO friendly URLs (generated dynamic by a sitemap or other mechanism)
- MVC paging.
Is there a resource (book, sample, training, etc.) that addresses this?
Help!!!
It’s a difficult experience for almost everybody to get by and propser in this market. Just a number of internet marketers and internet businesses really generate income. So, I appreciate your content and I wish this will guide other individuals also to produce their unique way to accomplishment. As a marketer I am even so a believer in the email marketing formula and I am also using safelist in my home business as an tool to get new potential clients. I lately happened upon a new one, an incredible new conception, WPSafelist, you would likely want to check it.
ÿþ|
Thank you for taking the time to put up this sample….so far it’s working great for me!!! What I had to do is start with a clean empty project and create one area at a time and baby step my way through it so I can truly understand what was happening regarding routing …when making my areas a little verbose for SEO I was getting tangled up but baby steps, THIS EXAMPLE, and bite size chunks was the key ….thank you again.