Steve Hodgkiss

Restful routing for ASP.NET MVC 2

March 21, 2010

Restful routing has a new syntax and better support for ASP.NET MVC 2 areas. Routes are now grouped into route sets and a route set can be composed of multiple route sets using the Connect<RouteSet>(“pathprefix”) method described later.

public class Routes : RouteSet
{
  public Routes()
  {
    Resources<BlogsController>(() =>
    {
      As("weblogs");
      Only("index", "show");
      Collection("latest", HttpVerbs.Get);
      Resources<PostsController>(() =>
      {
        Except("create", "update", "destroy");
        Resources<CommentsController>(() => Except("destroy"));
      });
    });
  }
}
public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new RestfulRoutingViewEngine());
    
    RouteTable.Routes.MapRoutes<Routes>();
    }
}

This will generate the following routes:

Http MethodPathEndpoint (controller#action)
GETweblogsblogs#index
GETweblogs/latestlatest
GETweblogs/{id}blogs#show
GETweblogs/{blogId}/postsposts#index
GETweblogs/{blogId}/posts/newposts#new
GETweblogs/{blogId}/posts/{id}/editposts#edit
GETweblogs/{blogId}/posts/{id}posts#show
GETweblogs/{blogId}/posts/{postId}/commentscomments#index
POSTweblogs/{blogId}/posts/{postId}/commentscomments#create
GETweblogs/{blogId}/posts/{postId}/comments/newcomments#new
GETweblogs/{blogId}/posts/{postId}/comments/{id}/editcomments#edit
GETweblogs/{blogId}/posts/{postId}/comments/{id}comments#show
PUTweblogs/{blogId}/posts/{postId}/comments/{id}comments#update

Http method overrides

Http methods can be overridden on POST requests by specifying a form parameter name _method with the value of the method you wanted to use. We need this because you can only use GET and POST as the HTTP method on HTML forms. There are a couple of helper methods available to generate these tags.

<%= Html.PutOverrideTag() %>
<input type="hidden" name="_method" value="put">
<%= Html.DeleteOverrideTag() %>
<input type="hidden" name="_method" value="delete">

Resources and Resource

Resources maps a collection of resources and Resource maps a singleton resource. You would use the singleton resource mapping for resources that the user only has one of. Session is a typical example of this.

Resource<SessionsController>(() =>
{
  Resource<AvatarsController>();
});

Note the controller name is still plural, as the application handles many of them, it is just the path to the resource that is singular. This produces the following routes:

Http MethodPathEndpoint (controller#action)
GETsessionsessions#show
POSTsessionsessions#create
GETsession/newsessions#new
GETsession/editsessions#edit
PUTsessionsessions#update
DELETEsessionsessions#destroy
GETsession/avataravatars#show
POSTsession/avataravatars#create
GETsession/avatar/newavatars#new
GETsession/avatar/editavatars#edit
PUTsession/avataravatars#update
DELETEsession/avataravatars#destroy

Areas

Area<Controllers.Admin.BlogsController>("admin", () =>
{
  Resources<BlogsController>(() =>
  {
    Resources<PostsController>(() =>
    {
      Resources<CommentsController>();
    });
  });
});

This maps an area called admin with the controllers located in the Controllers.Admin namespace. You can specify that the whole RouteSet is to be put into an area without putting a path prefix on the mapping. The following would just constrain all the routes to the namespace of BlogsController, but still look for views based on the area name blogs.

Area<BlogsController>("blogs");
Resources<BlogsController>();

If you want to nest mappings under a specific path you can use a non-namespaced area. This will just prepend admin/ to the blogs resource, without constraining to a namespace of controllers.

Area("admin", () => Resources<BlogsController>());

To generate URL’s for area’s you need to specify the are name

Url.Action("index", "blogs", new { area = "admin "}, new {})

Connecting route sets together

You can connect routes together using the Connect method.

public class BlogRoutes : RouteSet
{
  public BlogRoutes()
  {
    Area<BlogsController>("blogs");
    Resources<BlogsController>();
    Resources<PostsController>();
  }
}
public class Routes : RouteSet
{
  public Routes()
  {
    Connect<BlogRoutes>(""); // maps the BlogRoutes route set.
  }
}
// Application_Start()
RouteTable.Routes.MapRoutes<Routes>();

RestfulRoutingViewEngine

By default the WebformViewEngine looks for area views in this format: Areas/{area}/Views/{controller}/{action}. The RestfulRoutingViewEngine looks for views in this format: Views/{area}/{controller}/{action}. Using the RestfulRoutingViewEngine is completely optional.

Member and collection actions

Member actions are those which are to be performed on a member of the collection, and include the ID in the path. Collection actions are mapped against the collection and do not include the ID in the generated route.

Resources<BlogsController>(() =>
{
  Member("print", HttpVerbs.Get); // blogs/{id}/print
  Collection("latest", HttpVerbs.Get); // blogs/latest
});

Standard mappings

A way to specify standard mappings is also provided.

Map("posts/{year}/{slug}").To<PostsController>(x => x.Post(-1, "")).Constrain("slug", @"\w+").Constrain("year", @"\d+");

You can also add custom routes.

Route(new Route("posts/{action}", new RouteValueDictionary(new { controller = "posts" }), new MvcRouteHandler());

More information: