Steve Hodgkiss

RESTful routes for ASP.NET MVC

October 11, 2009

This post refers to an old version of restful routing, please see this updated post for a more recent reference.

Rails’ RESTful routing is something I miss when developing for ASP .NET MVC. The style it imposes on your application architecture has many benefits. I wanted this style for my ASP.NET MVC apps. I started looking at Adam Tybor’s SimplyRestfulRouting handler in MvcContrib. This works well for simple applications, but it doesn’t have a lot of the features/configuration options rails’ routing does. I needed more, so I decided to build my own.

RestfulRouting

RestfulRouting is based on Rails’ RESTful routing and has nested routes, shallow nested routes, singleton resources and a lot of configuration options. You map a resource like this:

var map = new RestfulRouteMapper(RouteTable.Routes);
map.Resources<BlogsController>();

This will map a blogs resource with the following routes.

Path Default Action Constraints
blogs index GET
blogs create POST
blogs/new new GET
blogs/{id}/action show GET, action = show,edit or delete
blogs/{id} update PUT
blogs/{id} destroy DELETE

It also includes a route to accept post overrides, which will let you specify put or delete on a POST request to a resource and have it map to update or destroy <input type="hidden" name="_method" value="delete" />. This is needed because HTML forms only support POST or GET.

Configuration

// maps it to admin/blogs
map.Resources<BlogsController>(config => config.PathPrefix = "admin"); 
// maps the resource as weblogs instead of blogs
map.Resources<BlogsController>(config => config.As = "weblogs");
// only accepts integer ids
map.Resources<BlogsController>(config => config.IdValidationRegEx = @"\d+");
// adds an extra collection action blogs/latest GET
map.Resources<BlogsController>(config => config.AddCollectionRoute("latest");
// excludes the delete route from the mappings
map.Resources<BlogsController>(config => config.Except("delete");
// only maps the index and show actions
map.Resources<BlogsController>(config => config.Only("index", "show"));
// adds an extra member action blogs/1/moveup that only accepts POSTs
map.Resources<BlogsController>(config => config.AddMemberRoute("moveup", HttpVerbs.Post));

Nested Resources

Resources can be nested

map.Resources<BlogsController>(m => {
  m.Resources<PostsController>(p => {
    p.Resources<CommentsController>();
  })
});

So a comment path would look like this

blogs/1/posts/2/comments/3

Shallow nested resources

If you don’t want to nest 3 levels down you can specify the shallow option.

map.Resources<BlogsController>(config => config.Shallow = true, m => {
  m.Resources<PostsController>(p => {
    p.Resources<CommentsController>();
  })
});

This maps the index of posts under a blog resource and at the root. You would have paths like this

blogs/1/posts
posts/1
posts/1/comments
comments/1

A more complex example

Route mappings for a case study, which has many images and videos. The CaseStudyImagesController is mapped as images and has a member action to get a thumbnail. The path for a case study thumbnail would be casestudies/2/images/3/thumb.

var map = new RestfulRouteMapper(routes);
map.Resources<CaseStudiesController>(study => {
  study.Resources<CaseStudyImagesController>(config => {
    config.As = "images";
    config.AddMemberRoute("thumb");
  });
  study.Resources<CaseStudyVideosController>(config => config.As = "videos");
});

More info and a sample project can be found on the GitHub project page.