9

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); */ }
Community
  • 1
  • 1
one.beat.consumer
  • 9,414
  • 11
  • 55
  • 98

1 Answers1

10

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.

Community
  • 1
  • 1
Lucero
  • 59,176
  • 9
  • 122
  • 152
  • Perhaps I'm missing something in your answer, but it doesn't make sense. in your example, each action has a different name and a different signature. I am essentially trying to have 4 separate actions with the same method signatures, each respond to a different http verb. They need to be separate actions because some of them will require authorization, others will not. See update. – one.beat.consumer Dec 23 '11 at 00:02
  • @one.beat.consumer: The point here is that the name of the method is *irrelevant*, because they all match to the same action names "<>" which is set statically via route. Therefore you can have any number of methods (and restful controllers) on one route, and you can have any number of "identical" actions (even though they have different method names) which corresponds to your solution A, or you can have multiple HTTP methods per method if you like which would be your solution B, and you can even mix these solutions as you see fit. – Lucero Dec 23 '11 at 00:07
  • @one.beat.consumer: (Ran out of space in the previous comment) Note that authorization is done on the method level, not action level, so that this will work just fine if you add an `[Authorize]` attribute to some of the verb methods. – Lucero Dec 23 '11 at 00:12
  • bare with me on this one. I am still not understanding you. While you were commenting I was updating my question with an example of what I am trying to achieve. For clarity, when I say "Action" I am talking about the "*method* on the controller" so I am not sure why you mention them separate in your comment. – one.beat.consumer Dec 23 '11 at 00:16
  • I did not know there was a difference between "Action Name" and "Action Method", that they could have a different value... what you are saying is I essentially can have any number of actions share the same "name" property, and yet their declarative method name `public ActionResult Method()` can be different from its "name"? – one.beat.consumer Dec 23 '11 at 00:25
  • @one.beat.consumer: The default is that the route contains the action as part of the URL, and that the method name is used for mapping to the action (by convention). Now as you have seen yourself this is useless for RESTful code, because in that case you want to use HTTP methods instead of actions to call the different methods on your controller. Therefore I add a static action name for RESTful calls (via route) and map the methods to match this static action name via my `[RestfulAction]` attribute. Not a hack really, basically just sensible use of the MVC extensibility points. – Lucero Dec 23 '11 at 00:25
  • @one.beat.consumer, the MVC framework also has the [`ActionNameAttribute`](http://msdn.microsoft.com/en-us/library/system.web.mvc.actionnameattribute.aspx) which allows you to have multiple methods with different names match to the same action. If not specified, the framework will use the method name as default. As for my code, using `[RestfulAction]` is equivalent (but more distinct) to `[ActionName("<>")]`. This action name attribute is designed for cases where you want to use the same method signature and action but differ by HTTP method: http://stackoverflow.com/questions/6348372 – Lucero Dec 23 '11 at 00:28
  • I think I'm understanding better. Technically I could use the `[ActionName("REST")]` on each action method in my controller and make sure the route pointed to "REST" as the action name? This would achieve the same thing you are doing just without referencing an attribute in global.asax right? – one.beat.consumer Dec 23 '11 at 00:30
  • it clicked right as you were typing. i'll mark yours as the answer if you'll do one edit... make it a 2 step example for other users (1) showing the importance of the action name vs. method name... and then the custom attribute for tidy code. Thank you for the help and patience. – one.beat.consumer Dec 23 '11 at 00:32
  • @one.beat.consumer, yes, but less concise. Note that the attribute has nothing to do with `global.asax` really, and you'd still need a custom route. The attribute (and the special action name) help prevent errors and make the code document itself. We have gone as far as registering the routes automatically by searching the assembly for matching controllers so that we can just add new controllers and the routes will be there without manual interaction, but that's out of the scope of this question. – Lucero Dec 23 '11 at 00:35