MVC3 RESTful API路由和HTTP动词处理

9
我想为我的MVC3应用程序构建一个RESTful Json Api。我需要处理多个Http动词来操作单个对象实例的帮助。
我已经阅读/学习/尝试了以下内容:
MVC属性(HttpGet,HttpPost等)允许我拥有具有相同名称但仍必须具有不同方法签名的多个操作的控制器。
路由约束在MVC启动之前在路由模块中发生,这将导致我有4个明确的路由,并且仍需要单独命名控制器操作。
构建自定义Http动词属性可用于捕获用于访问操作的动词,然后将其作为参数传递给调用操作 - 代码将处理switch案例。此方法的问题是某些方法将需要授权,这应该在操作过滤器级别处理,而不是在操作本身内部处理。 ASP.NET MVC AcceptVerbs and registering routes

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


需求/目标

  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(); }
    

问题

如何让多个动作(具有相同名称和方法签名)分别响应不同的HTTP动词。期望的示例:

[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个回答

10

对于RESTful调用而言,动作本身并没有意义,因为您只想通过HTTP方法来区分它们。所以诀窍是使用一个静态的动作名称,以便控制器上不同的方法仅在它们接受的HTTP方法上有所不同。

虽然MVC框架提供了指定动作名称的解决方案,但可以使其更简洁和自我解释。我们这样解决:

使用特殊属性来指定RESTful方法(这与特殊动作名称匹配):

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

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

控制器与HTTP方法属性相结合使用:

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(...);
    }
}

为了成功绑定这些控制器,我们使用自定义路由和之前提到的属性中的静态动作名称(同时也允许自定义URL):

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

请注意,据我所知,使用这种方法可以轻松满足您的所有要求;您可以在一个方法上具有多个[HttpXxx]属性,以使一个方法接受多个HTTP方法。再配合一些更智能的ModelBinder,这非常强大。


@one.beat.consumer:(在上一个评论中已经没有空间了)请注意,授权是在方法级别而不是操作级别上完成的,因此如果您向某些动词方法添加[Authorize]属性,这将完美地工作。 - Lucero
请耐心等待。我仍然不理解你的意思。当你在评论时,我正在使用一个示例更新我的问题。为了清楚起见,当我说“操作”时,我指的是控制器上的“方法”,所以我不确定你为什么在评论中将它们分开提到。 - one.beat.consumer
@one.beat.consumer:默认情况下,路由包含操作作为URL的一部分,并且方法名称用于映射到操作(按照惯例)。现在,正如您自己所看到的,这对于RESTful代码是无用的,因为在这种情况下,您希望使用HTTP方法而不是操作来调用控制器上的不同方法。因此,我通过我的[RestfulAction]属性为RESTful调用添加了一个静态操作名称,并将方法映射到与此静态操作名称匹配。这不是一个技巧,基本上只是合理地使用MVC可扩展性点。 - Lucero
我觉得我理解得更好了。从技术上讲,我可以在控制器中的每个操作方法上使用[ActionName("REST")],并确保路由将“REST”作为操作名称?这样做可以实现与您所做的相同的效果,但无需引用global.asax中的属性,对吗? - one.beat.consumer
@one.beat.consumer,是的,但不够简洁。请注意,该属性实际上与global.asax无关,您仍需要自定义路由。该属性(以及特殊的操作名称)有助于防止错误并使代码自文档化。我们甚至已经通过搜索程序集来注册匹配控制器的路由,以便我们可以添加新的控制器而无需手动干预,但这超出了本问题的范围。 - Lucero
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接