MVC3 RESTful API Routing & Http Verb Handling

2020-07-06 05:10发布

问题:

I want to build a RESTful Json Api for my MVC3 application. I need help with handling multiple Http Verbs for the manipulation of a single object instance.

What I've read/studied/tried

MVC attributes (HttpGet, HttpPost, etc.) allow me to have a controller with multiple actions sharing the same name, but they still must have different method signatures.

Route constraints happen in the routing module before MVC kicks in and would result in me having 4 explicit routes, and still require individually named controller actions.

ASP.NET MVC AcceptVerbs and registering routes

Building a custom Http Verb Attribute could be used to snatch the verb used to access the action and then pass it as an argument as the action is invoked - the code would then handle switch cases. The issue with this approach is some methods will require authorization which should be handled at the action filter level, not inside the action itself.

http://iwantmymvc.com/rest-service-mvc3


Requirements / Goals

  1. One route signature for a single instance object, MVC is expected to handle the four main Http Verbs: GET, POST, PUT, DELETE.

    context.MapRoute("Api-SingleItem", "items/{id}", 
        new { controller = "Items", action = "Index", id = UrlParameter.Optional }
    );
    
  2. When the URI is not passed an Id parameter, an action must handle POST and PUT.

    public JsonResult Index(Item item) { return new JsonResult(); }
    
  3. When an Id parameter is passed to the URI, a single action should handle GET and DELETE.

    public JsonResult Index(int id) { return new JsonResult(); }
    

Question

How can I have more than one action (sharing the same name and method signature) each respond to a unique http verb. Desired example:

[HttpGet]
public JsonResult Index(int id) { /* _repo.GetItem(id); */}

[HttpDelete]
public JsonResult Index(int id) { /* _repo.DeleteItem(id); */ }

[HttpPost]
public JsonResult Index(Item item) { /* _repo.addItem(id); */}

[HttpPut]
public JsonResult Index(Item item) { /* _repo.updateItem(id); */ }

回答1:

For RESTful calls, the action has no meaning, since you want to differ only by HTTP methods. So the trick is to use a static action name, so that the different methods on the controller are only different in the HTTP method they accept.

While the MVC framework provides a solution for specifying action names, it can be made more concise and self-explaining. We solved it like this:

A special attribute is used for specifying RESTful methods (this matches to a special action name):

public sealed class RestfulActionAttribute: ActionNameSelectorAttribute {
    internal const string RestfulActionName = "<<REST>>";

    public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) {
        return actionName == RestfulActionName;
    }
}

The controllers use it in combination with the HTTP method attributes:

public class MyServiceController: Controller {
    [HttpPost]
    [RestfulAction]
    public ActionResult Create(MyEntity entity) {
        return Json(...);
    }

    [HttpDelete]
    [RestfulAction]
    public ActionResult Delete(Guid id) {
        return Json(...);
    }

    [HttpGet]
    [RestfulAction]
    public ActionResult List() {
        return Json(...);
    }

    [HttpPut]
    [RestfulAction]
    public ActionResult Update(MyEntity entity) {
        return Json(...);
    }
}

And in order to bind those controllers successfully, we use custom routes with the static action name from the beforementionned attribute (which at the same time also allow for customizing the URLs):

routes.MapRoute(controllerName, pathPrefix+controllerName+"/{id}", new {
    controller = controllerName,
    action = RestfulActionAttribute.RestfulActionName,
    id = UrlParameter.Optional
});

Note that all your requirements can be easily met with this approach as far as I can tell; you can have multiple [HttpXxx] attributes on one method to make one method accept multiple HTTP methods. Paired with some smart(er) ModelBinder this is very powerful.