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

时间:2012-03-07 01:44:24

标签: asp.net-mvc rest asp.net-web-api

新的ASP.net MVC 4 WebApi是否有更好的方法来处理嵌套资源,而不是为每个资源设置特殊路由? (类似于此处:ASP.Net MVC support for Nested Resources? - 这是2009年发布的)。

例如我想处理:

/customers/1/products/10/

我已经看到了ApiControllerGet()之外的Post()行为而不是GetOrder()等行为的示例,例如here我看到了一个名为{{1}的行动示例}。我找不到任何关于此的文档。这是实现这个目标的方法吗?

4 个答案:

答案 0 :(得分:37)

抱歉,我已多次更新此内容,因为我自己找到了解决方案。

似乎有很多方法可以解决这个问题,但到目前为止我发现的效率最高的是:

在默认路线下添加:

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

然后,此路由将匹配URL中的任何控制器操作和匹配的段名称。例如:

/ api / customers / 1 /订单将匹配:

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]等。

答案 1 :(得分:10)

编辑:虽然这个答案仍适用于Web API 1,但对于Web API 2,我强烈建议使用Daniel Halan's answer,因为它是映射子资源的最先进技术(以及其他细节) )。


有些人不喜欢在Web API中使用{action},因为他们认为这样做会打破REST“意识形态”......我认为这一点。 {action}仅仅是一个有助于路由的构造。它是您的实现内部,与用于访问资源的HTTP谓词无关。

如果你对动作加上HTTP谓词约束并相应地命名它们,你就不会破坏任何RESTful指南,最终会得到更简单,更简洁的控制器,而不是每个子资源的大量单独控制器。请记住:操作只是一种路由机制,它是您实现的内部机制。如果你反对框架,那么框架或你的实现就会出现问题。只需使用HTTPMETHOD约束映射路由,您就可以了:

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",  }
);

并在客户控制器中进行相应处理:

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(映射到GetOrders(long)返回客户1的所有订单)

GET http://your.api/customers/1/orders/22(映射到GetOrders(long,long),返回客户1的订单22

POST http://your.api/customers/1/orders(映射到CreateOrder(long),它将创建一个订单并将其返回给调用者(刚刚创建了新ID)

但是不要把我的话当作绝对真理。我还在尝试它,我认为MS无法正确处理子资源访问。

我敦促你尝试http://www.servicestack.net/来减少编写REST apis的痛苦经历......但是不要误会我的意思,我喜欢Web API并将其用于我的大多数专业项目,主要是因为它更容易找到已经“知道”它的程序员......对于我的个人项目,我更喜欢ServiceStack。

答案 2 :(得分: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();

答案 3 :(得分:5)

我不喜欢在ASP.NET Web API的路径中使用“actions”的概念。 REST中的操作应该是HTTP Verb。我通过简单地使用父控制器的概念,以一种通用且有点优雅的方式实现了我的解决方案。

https://stackoverflow.com/a/15341810/326110

以下是完整转载的答案,因为当一篇文章回答两个SO问题时,我不知道该怎么做:(


我希望以更一般的方式处理这个问题,而不是像Abhijit Kadam那样直接用controller = "Child"连接ChildController。我有几个子控制器,并且不想为每个控制器映射一个特定的路由,而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
}

我的实施的一些缺点

  • 如您所见,我使用了enum,因此我仍然需要在两个不同的位置管理父控制器。它可以很容易地成为字符串参数,但我想阻止api/crazy-non-existent-parent/5/comment/122工作。
  • 可能有一种方法可以使用反射或其他东西来动态地执行此操作而无需进行单独管理,但这对我来说现在很有用。
  • 它不支持儿童。

可能有一个更好的解决方案甚至更普遍,但就像我说的,这对我有用。