4
votes

I have an ASP.net MVC 3 Site with routes like this:

routes.MapRoute("Get", "endpoint/{id}",
    new { Controller = "Foo", action = "GetFoo" },
    new { httpMethod = new HttpMethodConstraint("GET") });

routes.MapRoute("Post", "endpoint/{id}",
    new { Controller = "Foo", action = "NewFoo" },
    new { httpMethod = new HttpMethodConstraint("POST") });

routes.MapRoute("BadFoo", "endpoint/{id}",
    new { Controller = "Error", action = "MethodNotAllowed" });

routes.MapRoute("NotFound", "", 
    new { controller = "Error", action = "NotFound" });

So in a Nutshell, I have a Route that matches on certain HTTP Verbs like GET and POST but on other HTTP Verbs like PUT and DELETE it should return a specific error.

My default Route is a 404.

If I remove the "BadFoo" route, then a PUT against endpoint/{id} returns a 404 because none of the other routes match, so it goes to my NotFound route.

The thing is, I have a ton of routes like Get and Post where I have an HttpMethodConstraint and where I would have to create a route like the BadFoo route just to catch a correct match on the route string but not on the Method, which blows up my routing unnecessarily.

How could I setup routing with only the Get, Post and NotFound routes while still differentiating between a HTTP 404 not found (=invalid URL) and HTTP 405 Method not allowed (=valid URL, wrong HTTP Method)?

1
What happens if you explicitly mark your controller methods with [HttpGet] and [HttpPost] rather than using routes? Will that make the PUT and DELETE request throw the correct exception? - Hector Correa
Someone raised this as a bug on MS Connect (I agree, they have not implemented the HTTP response codes correctly in this case). However it was closed it as by design - apparently they'd rather you write one-liner "throw 405 method not allowed" controller methods for all verbs. But I prefer Max's workaround below. Link: connect.microsoft.com/VisualStudio/feedback/details/419729/… - Richard Dingwall

1 Answers

4
votes

Instead of using route constraint you can delegate the HTTP method validation to the MVC runtime, using a custom ControllerActionInvoker and ActionMethodSelector attributes like [HttpGet], [HttpPost], etc.:

using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication6.Controllers {

   public class HomeController : Controller {

      protected override IActionInvoker CreateActionInvoker() {
         return new CustomActionInvoker();
      }

      [HttpGet]
      public ActionResult Index() {
         return Content("GET");
      }

      [HttpPost]
      public ActionResult Index(string foo) {
         return Content("POST");
      }
   }

   class CustomActionInvoker : ControllerActionInvoker {

      protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName) {

         // Find action, use selector attributes
         var action = base.FindAction(controllerContext, controllerDescriptor, actionName);

         if (action == null) {

            // Find action, ignore selector attributes
            var action2 = controllerDescriptor
               .GetCanonicalActions()
               .FirstOrDefault(a => a.ActionName.Equals(actionName, StringComparison.OrdinalIgnoreCase));

            if (action2 != null) {
               // Action found, Method Not Allowed ?
               throw new HttpException(405, "Method Not Allowed");
            }
         }

         return action;
      }
   }
}

Note my last comment 'Action found, Method Not Allowed ?', I wrote this as a question because there can be ActionMethodSelector attributes that are not related with HTTP method validation...