ASP.net MVC 4 WebApi中的嵌套资源

36
在新的ASP.net MVC 4 WebApi中,处理嵌套资源是否有比为每个资源设置特殊路由更好的方法?(类似于这里:ASP.Net MVC支持嵌套资源? - 这是2009年发布的)。例如,我想要处理:/customers/1/products/10/。我看到了一些命名不同于Get()Post()等的ApiController动作的示例,例如这里,我看到一个名为GetOrder()的操作示例。但我找不到任何文档。这是实现这一点的方法吗?

1
Action Invoker 只是查找 Get、Put、Post 方法作为前缀。方法的其余部分可以是任何内容。 - Darrel Miller
4个回答

37

很抱歉,我已经多次更新了这个问题,因为我自己正在寻找解决方案。

似乎有很多方法可以解决这个问题,但迄今为止我发现最有效的方法是:

在默认路由下添加以下内容:

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/{controller}/{customerId}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

这个路由会匹配URL中任何控制器操作和匹配的片段名称。例如:

/api/customers/1/orders 会匹配:

public IEnumerable<Order> Orders(int customerId)

/api/customers/1/orders/123将匹配:

public Order Orders(int customerId, int id)

/api/customers/1/products将匹配:

public IEnumerable<Product> Products(int customerId)

/api/customers/1/products/123将匹配:

public Product Products(int customerId, int id)

方法名必须与路由中指定的 {action} 段匹配。


重要提示:

从评论中得知

自 RC 版本开始,您需要告诉每个操作可接受哪种动词,例如 [HttpGet] 等。


你刚刚帮我避免了一个可怕的头痛。谢谢! - Adam
3
在RC中,您需要告诉每个操作可接受的动词类型,例如[AcceptVerbs("GET","POST")]. - Lars Nyström
如果您只支持一个动词,可以使用[HttpGet]代替[AcceptVerbs("GET")]。参考链接:https://dev59.com/tW445IYBdhLWcg3wq8Lp - foolshat
我唯一看到的问题是 public void Put(int id){} 会匹配 /controller/id 和 /controller/id/Put 两个url。你有什么想法吗? - Adam
@Adam 在操作上添加一个限制条件,拒绝值为 Get、POST、PUT。 - moi_meme

11

编辑:虽然本答案仍适用于Web API 1,但对于Web API 2,我强烈建议使用Daniel Halan的答案,因为它是映射子资源(以及其他好处)的最新技术。


有些人不喜欢在Web API中使用{action},因为他们认为这样做会破坏REST的“思想”...... 我不同意。 {action}只是一个有助于路由的构造。它是内部实现的一部分,与用于访问资源的HTTP动词无关。

如果对操作施加HTTP动词约束并相应地命名,您将不会违反任何RESTful指南,并且最终会得到更简洁的控制器,而不是每个子资源都有大量单独的控制器。记住:操作只是路由机制,它是您实现的一部分。如果你与框架作斗争,那么可能是框架或您的实现出了问题。只需使用HTTP方法约束映射路由即可:

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/customers/{customerId}/orders/{orderId}",
    constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "GET" }) },
    defaults: new { controller = "Customers", action = "GetOrders", orderId = RouteParameter.Optional,  }
);

您可以像这样在 CustomersController 中处理它们:

public class CustomersController
{
    // ...
    public IEnumerable<Order> GetOrders(long customerId)
    {
        // returns all orders for customerId!
    }
    public Order GetOrders(long customerId, long orderId)
    {
        // return the single order identified by orderId for the customerId supplied
    }
    // ...
}

您还可以将“创建”操作路由到同一个“资源”(订单):

routes.MapHttpRoute(
    name: "OneLevelNested",
    routeTemplate: "api/customers/{customerId}/orders",
    constraints: new { httpMethod = new HttpMethodConstraint(new string[] { "POST" }) },
    defaults: new { controller = "Customers", action = "CreateOrder",  }
);

在Customer控制器中相应地处理它:

public class CustomersController
{
    // ...
    public Order CreateOrder(long customerId)
    {
        // create and return the order just created (with the new order id)
    }
    // ...
}

是的,你仍然需要创建很多路由,因为Web API仍然无法根据路径路由到不同的方法... 但我认为用声明性方式定义路由比基于枚举或其他技巧的自定义分派机制更加清晰。
对于API使用者来说,它看起来完全符合RESTful: GET http://your.api/customers/1/orders (映射到返回客户1的所有订单的GetOrders(long)) GET http://your.api/customers/1/orders/22 (映射到返回客户1的订单22的GetOrders(long, long)) POST http://your.api/customers/1/orders (映射到CreateOrder(long),该方法将创建订单并将其返回给调用者(包括新创建的ID))
但是不要把我的话当成绝对真理。我仍在实验中,我认为微软没有妥善解决子资源访问问题。
我敦促你尝试一下http://www.servicestack.net/,写REST api会少些痛苦......但是别误会,我喜欢Web API,并在大多数专业项目中使用它,主要是因为更容易找到已经"知道"它的程序员......对于我的个人项目,我更喜欢ServiceStack。

8
自从Web API 2起,您可以使用路由属性来定义每个方法的自定义路由,从而实现分层路由。
public class CustomersController : ApiController
{
    [Route("api/customers/{id:guid}/products")]
    public IEnumerable<Product> GetCustomerProducts(Guid id) {
       return new Product[0];
    }
}

你还需要在WebApiConfig.Register()中初始化属性映射。
  config.MapHttpAttributeRoutes();

据我所知,这是使用ASP.Net Web API 2的最先进解决方案,也是其他框架(如Servicestack或几个Java-REST-Frameworks)处理它的方式。更多信息可以在此处找到:http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2 - Ursin Brunner
刚刚发现了这个答案,我必须更新我的答案并引用你的。这比在Web API中使用“action”更加优雅且具有更多的灵活性。而且实现起来也更容易!不错! - Loudenvier

5
我不喜欢在ASP.NET Web API的路由中使用“actions”这个概念。在REST中,动作应该是HTTP动词。我通过使用父控制器的概念以一种通用且优雅的方式实现了我的解决方案。

https://dev59.com/Smgv5IYBdhLWcg3wMN60#15341810

以下是完整的答案,因为我不确定当一篇帖子回答两个SO问题时该怎么做:(


我希望以更加通用的方式处理这个问题,而不是像Abhijit Kadam那样直接将ChildController与controller = "Child"连接起来。我有几个子控制器,不想为每个子控制器都映射一个特定的路由,反复使用controller = "ChildX"controller = "ChildY"
我的WebApiConfig如下所示:
config.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional }
);
  config.Routes.MapHttpRoute(
  name: "ChildApi",
  routeTemplate: "api/{parentController}/{parentId}/{controller}/{id}",
  defaults: new { id = RouteParameter.Optional }
);

我的父级控制器非常标准,并且与上面的默认路由匹配。示例子控制器如下:

public class CommentController : ApiController
{
    // GET api/product/5/comment
    public string Get(ParentController parentController, string parentId)
    {
        return "This is the comment controller with parent of "
        + parentId + ", which is a " + parentController.ToString();
    }
    // GET api/product/5/comment/122
    public string Get(ParentController parentController, string parentId,
        string id)
    {
        return "You are looking for comment " + id + " under parent "
            + parentId + ", which is a "
            + parentController.ToString();
    }
}
public enum ParentController
{
    Product
}

我实现的一些缺点

  • 正如您所看到的,我使用了一个枚举,因此仍需要在两个独立的位置管理父控制器。它本可以很容易地成为一个字符串参数,但我想防止api/crazy-non-existent-parent/5/comment/122发挥作用。
  • 可能有一种方法可以使用反射或其他东西来动态完成这项工作,而无需单独管理它,但是目前对我来说这样做就足够了。
  • 它不支持子级的子级。

可能有更好的解决方案,甚至更通用,但是正如我所说,这对我来说已经足够了。


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