RESTful Routing in ASP.NET MVC 2 Preview 2

Microsoft recently released Preview 2 of the next version of their ASP.NET MVC framework. There’s a couple of things in this release that are designed to allow your controls to expose RESTful APIs, and – more interestingly, I think – to let you build your own Web pages and applications on top of the same controllers and routing set-up that provides this RESTful API. In other words, you can build one RESTful API exposing your business logic and domain methods, and then your own UI layer – your views and pages – can be implemented on top of this same API that you’re exposing for developers and third parties.

Thing is… I think they way they’ve implemented it in preview  doesn’t really work. Don’t get me wrong; there are some good ideas in there – among them an HTML helper method, Html.HttpMethodOverride, that works with the MVC routing and controller back-end to “simulate” unsupported HTTP verbs on browsers that don’t support them (which was all of them, last time I looked). You write your form code like this:

<form action=”/products/1234” method=”post”>
    <%= Html.HttpMethodOverride(HttpVerbs.Delete) %>
    <input type=”submit” name=”whatever” value=”Delete This Product” />
</form>

and then in your controller code, you implement a method something like:

[AcceptVerbs(HttpVerbs.Delete)]
public ActionResult Delete(int id) {
  /* delete product here */
   return(Index());
}  

The London Eye and Houses of Parliament by night. Very restful.The HTML helper injects a hidden form element called X-HTTP-Method-Override into your POST submission, and then the framework examines that hidden field when deciding whether your request should pass the AcceptVerbs attribute filter on a particular method.

Now, most MVC routing examples – and the default behaviour you get from the Visual Studio MVC file helpers – will give you a bunch of URLs mapped to different controller methods using a {controller}/{action}/{id} convention – so your application will expose URLs that look like this:

  • /products/view/1234
  • /products/edit/1234
  • /products/delete/1234

Since web browsers only support GET and POST, we end up having to express our intentions through the URI like this, and so the URI doesn’t really identify a resource, it identifies the act of doing something to a resource. That’s all very well if you subscribe to the Nathan Lee Chasing His Horse school of nomenclature, but one of the key tenets of REST is that you can apply a different verb to the same resource identifier – i.e. the same URI – in order to perform different operations. Assuming we’re using the product ID as part of our resource identification system, then:

  • PUT /products/1234 – will create a new product with ID 1234
  • POST /products/1234 – will update product #1234
  • GET /products/1234 – will retrieve a representation of product #1234
  • DELETE /products/1234 – will remove product #1234

One approach would be to map all these URIs to the same controller method – say ProductController.DoTheRightThing(int id) – and then inspect the Request.HttpMethod inside this method to see whether we’re PUTing, POSTing, or what.

This won’t work, though, because Request.HttpMethod hasn’t been through the ‘unsupported verb translator’ that’s included with MVC 2; the Request.HttpMethod will still be “POST” even if the request is a pseudo-DELETE created via the HttpMethodOverride technique shown above.

Now, MVC v1 supports something called route constraints. Stephen Walther has a great post about these; basically they’ll let you say that a certain route only applies to GET requests or POST requests.

routes.MapRoute(
    "Product", 
    "Product/Insert",
    new { controller = "Product", action = "Insert"},
    new { httpMethod = new HttpMethodConstraint("POST") }
);

That last line there? That’s the key – you can map a request for /Product/1234 to your controller’s Details() method if the request is a GET request, and map the same URL - /Product/1234 – to your controller’s Update() method if the request is a POST request. Very nice, and very RESTful.

But – yes, you guessed it; it doesn’t work with PUT and DELETE, because it’s still inspecting the untranslated Request.HttpMethod, which will always be GET or POST with today’s browsers.

However, thanks to the ASP.NET MVC’s rich extensibility, it’s actually very simple to add the support we need alongside the features built in to preview 2. (So simple that this started out as a post complaining that MVC2 couldn’t do it, until I realized I could probably implement what was missing in less time than it would take to describe the problem)

You’ll need to brew yourself up one of these:

/// Allows you to define which HTTP verbs are permitted when determining 
/// whether an HTTP request matches a route. This implementation supports both 
/// native HTTP verbs and the X-HTTP-Method-Override hidden element
/// submitted as part of an HTTP POST
public class HttpVerbConstraint : IRouteConstraint {

  private HttpVerbs verbs;

  public HttpVerbConstraint(HttpVerbs routeVerbs) {
    this.verbs = routeVerbs;
  }

  public bool Match(
HttpContextBase httpContext,
Route route, string parameterName, RouteValueDictionary values,
RouteDirection routeDirection
) { switch (httpContext.Request.HttpMethod) { case "DELETE": return ((verbs & HttpVerbs.Delete) == HttpVerbs.Delete); case "PUT": return ((verbs & HttpVerbs.Put) == HttpVerbs.Put); case "GET": return ((verbs & HttpVerbs.Get) == HttpVerbs.Get); case "HEAD": return ((verbs & HttpVerbs.Head) == HttpVerbs.Head); case "POST": // First, check whether it's a real post. if ((verbs & HttpVerbs.Post) == HttpVerbs.Post) return (true); // If not, check for special magic HttpMethodOverride hidden fields. switch (httpContext.Request.Form["X-HTTP-Method-Override"]) { case "DELETE": return ((verbs & HttpVerbs.Delete) == HttpVerbs.Delete); case "PUT": return ((verbs & HttpVerbs.Put) == HttpVerbs.Put); } break; } return (false); } }

This just implements the IRouteConstraint interface (part of MVC) with a Match() method that will check for the hidden form field when deciding whether to treat a POST request as a pseudo-DELETE or pseudo-PUT. Once you’ve added this to your project, you can set up your MVC routes like so:

routes.MapRoute(
  // Route name - anything you like but must be unique.
  "DeleteProduct",				 
  
  // The URL pattern to match
  "Products/{guid}", 
  
  // The controller and method that should handle requests matching this route 
  new { controller = "Products", action = "Delete", id = "" },   
  
  // The HTTP verbs required for a request to match this route.
  new { httpVerbs = new HttpVerbConstraint(HttpVerbs.Delete) }
);

routes.MapRoute(
  "CreateProduct",
  "Products/{id}",
  new { controller = "Products", action = "Create", id = "" },
  new { httpVerbs = new HttpVerbConstraint(HttpVerbs.Put) }
);

routes.MapRoute(
  "DisplayProduct",
  "Products/{id}",
  new { controller = "Products", action = "Details", id = "" },
  new { httpVerbs = new HttpVerbConstraint(HttpVerbs.Get) }
);

and finally, just implement your controller methods something along these lines:

public class ProductsController {
  public ViewResult Details(int id) { /* implementation */ }
  public ViewResult Create(int id) { /* implementation */ }
  public ViewResult Delete(int id) { /* implementation */ }
}

You don’t need the AcceptVerbs attribute at all. I think you’re better off mapping each resource/verb combination to sensibly-named method on your controller, and leaving it at that. Let proper REST clients send requests using whichever verb they like; let normal browsers submit POSTs with hidden X-HTTP-Method-Override fields, trust the routing engine and route constraints to sort that lot out before it hits your controller code, and you’ll find that you can completely decouple your resource identification strategy from your controller/action naming conventions.

BLATANT PLUG: If you’re into this kind of thing, you should come along to Skills Matter in London on November 2nd, where I’ll be talking about the future of web development - HTML 5, MVC 2, REST, jQuery, semantic markup, web standards, and… well, you’ll have to come along and find out. If you’re interested, register here and see you on the 2nd.)