OdeToCode IC Logo

ASP.NET MVC2 Preview 2: Areas and Routes

Tuesday, October 13, 2009

In ASP.NET web forms I’ve used the “sub-web project” trick to break apart large web applications. It has some downsides, but generally works. ASP.NET MVC 2 will include built-in support for breaking apart a large MVC application into “areas”. To quote ScottGu:

Areas provide a means of grouping controllers and views to allow building subsections of a large application in relative isolation to other sections. Each area can be implemented as a separate ASP.NET MVC project which can then be referenced by the main application. This helps manage the complexity when building a large application and facilitates multiple teams working together on a single application together.

There is a detailed walkthrough on MSDN for creating an Areas application using multiple projects. You create a parent project (MvcAreasMultiProject) and two sub-projects (Accounts and Store).

  • The parent project includes the usual Home and Account controllers (and associated views).
  • The Store project includes a Product controller (and its views).
  • The Accounts project maintains an Accounts controller (and its views).

Notice the “s” on the Accounts controller name – there is an Account controller and an Accounts controller in the application – we’ll come back to that.

Inside the parent project, use the AreaRegistration class to magically register all the routes in all child projects.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    AreaRegistration.RegisterAllAreas();

    routes.MapRoute(
        "Default",                                            
        "{controller}/{action}/{id}",                         
        new { controller = "Home", action = "Index", id = "" }
    );
}

The AreaRegistration class will scan assemblies looking for types derived from AreaRegistration. It will instantiate these types and execute a method to  give the the child projects a change to register their own routes. For example, the Accounts sub-project registers it’s routes with the following code:

public class Routes : AreaRegistration
{
    public override string AreaName
    {
        get { return "Accounts"; }
    }

    public override void RegisterArea(
                           AreaRegistrationContext context)
    {
        context.MapRoute(
            "Accounts_Default",
            "Profile/{action}/{id}",
            new { controller = "Accounts", 
                  action = "Index", id = "" }
        );
    }
}

With routes in place you can now generate links that reach specific areas. The following snippet creates a link to the Accounts controller in the Accounts sub-project…

<%= Html.ActionLink("Accounts", "Index", "Accounts",
    new { area = "accounts" }, null)%>

… while this one links to the Account controller in the parent project…

<%= Html.ActionLink("Log On", "LogOn", "Account", 
              new { area = "" }, null)%>

Conflict!

What happens if two of the projects have a controller with the same name? For instance, if both the parent project and the Accounts project have an AccountController (with no “s” on Account).

If you try to reach the AccountController inside the Accounts area - everything should work. This is because the AreaRegistrationContext used to register routes for the Accounts area is automatically adding a namespace value to restrict the controller search. It’s like using the following code:

namespace Accounts 
    //      ^^
    // the namespace of the area registration type
    // is used by default when mapping routes
{
    public class Routes : AreaRegistration
    {
        public override string AreaName
        {
            get { return "Accounts"; } 
        }

        public override void 
            RegisterArea(AreaRegistrationContext context)                
        {                        
            context.MapRoute(
                "Accounts_Default",
                "Profile/{action}/{id}",
                new { controller = "Account", 
                      action = "Index", id = "" }, 
                null,
                new string[] { "Accounts" } 
                // ^ this is explicitly specifying
                // the namespace as "Accounts", 
                // but "Accounts" is the 
                // default, so this isn't needed
            );
        }
    }
}

Note that the AccountController doesn’t have to live in the root Accounts namespace. It can live further down the hierarchy (like Accounts.Controllers.AccountController) and the factory will still find it.

The child area is in good shape, but a problem can occur if you try to reach the AccountController in the parent project. If the routes for the parent project were not given a any namespace values (which they aren’t by default), then  the default controller factory will become angry and throw an exception at runtime.

The controller name 'Account' is ambiguous between the following types:
MvcAreasMultiProject.Controllers.AccountController
Accounts.Controllers.AccountController

The easiest solution is to include the namespace(s) for your parent project controllers when registering routes in the parent area.

routes.MapRoute(
    "Default",                                             
    "{controller}/{action}/{id}",                          
    new { controller = "Home", action = "Index", id = "" },
    null, 
    // namespaces ->
    new string[] { "MvcAreasMultiProject" }
);

Summary

  • You need to specify namespaces when registering routes if you have duplicate controller names.
  • The AreaRegistrationContext will automatically include a namespace value when registering routes in a child area. The namespace is the same namespace as the type used to register routes.

Coming up next Tuesday, my answers to the following questions:

  • How does this area stuff impact my use of an IoC container?
  • How can I write tests to avoid ambiguous controller name exceptions at runtime?