Nested resources in ASP.net MVC 4 WebApi

2019-01-10 05:02发布

Is there a better way in the new ASP.net MVC 4 WebApi to handle nested resources than setting up a special route for each one? (similar to here: ASP.Net MVC support for Nested Resources? - this was posted in 2009).

For example I want to handle:

/customers/1/products/10/

I have seen some examples of ApiController actions named other than Get(), Post() etc, for example here I see an example of an action called GetOrder(). I can't find any documentation on this though. Is this a way to achieve this?

4条回答
何必那么认真
2楼-- · 2019-01-10 05:39

EDIT: Although this answer still applies for Web API 1, for Web API 2 I strongly advise using Daniel Halan's answer as it is the state of the art for mapping subresources (among other niceties).


Some people don't like to use {action} in Web API because they believe that in doing so they will be breaking the REST "ideology"... I contend that. {action} is merely a construct that helps in routing. It is internal to your implementation and has nothing to do with the HTTP verb used to access a resource.

If you put HTTP verb constraints on the actions and name them accordingly you're not breaking any RESTful guidelines and will end up with simpler, more concise controllers instead of tons of individual controllers for each sub-resource. Remember: the action is just a routing mechanism, and it is internal to your implementation. If you struggle against the framework, then something is amiss either with the framework or your implementation. Just map the route with an HTTPMETHOD constraint and you're good to go:

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

You can handle these in the CustomersController like this:

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
    }
    // ...
}

You can also route a Create action on the same "resource" (orders):

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

And handle it accordingly in the Customer controller:

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

Yes, you still have to create a lot of routes just because Web API still can't route to different methods depending on the path... But I think it is cleaner to declaratively define the routes than to come up with a custom dispatching mechanisms based on enums or other tricks.

For the consumer of your API it will look perfectly RESTful:

GET http://your.api/customers/1/orders (maps to GetOrders(long) returning all orders for customer 1)

GET http://your.api/customers/1/orders/22 (maps to GetOrders(long, long) returning the order 22 for customer 1

POST http://your.api/customers/1/orders (maps to CreateOrder(long) which will create an order and return it to the caller (with the new ID just created)

But don't take my word as an absolute truth. I'm still experimenting with it and I think MS failed to address properly subresource access.

I urge you to try out http://www.servicestack.net/ for a less painful experience writing REST apis... But don't get me wrong, I adore Web API and use it for most of my professional projects, mainly because it is easier to find programmers out there that already "know" it... For my personal projects I prefer ServiceStack.

查看更多
太酷不给撩
3楼-- · 2019-01-10 05:55

Since Web API 2 you can use Route Attributes to define custom routing per Method, allowing for hierarchical routing

public class CustomersController : ApiController
{
    [Route("api/customers/{id:guid}/products")]
    public IEnumerable<Product> GetCustomerProducts(Guid id) {
       return new Product[0];
    }
}

You also need to initialize Attribute Mapping in WebApiConfig.Register(),

  config.MapHttpAttributeRoutes();
查看更多
别忘想泡老子
4楼-- · 2019-01-10 06:01

Sorry, I have updated this one multiple times as I am myself finding a solution.

Seems there is many ways to tackle this one, but the most efficient I have found so far is:

Add this under default route:

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

This route will then match any controller action and the matching segment name in the URL. For example:

/api/customers/1/orders will match:

public IEnumerable<Order> Orders(int customerId)

/api/customers/1/orders/123 will match:

public Order Orders(int customerId, int id)

/api/customers/1/products will match:

public IEnumerable<Product> Products(int customerId)

/api/customers/1/products/123 will match:

public Product Products(int customerId, int id)

The method name must match the {action} segment specified in the route.


Important Note:

From comments

Since the RC you'll need to tell each action which kind of verbs that are acceptable, ie [HttpGet], etc.

查看更多
够拽才男人
5楼-- · 2019-01-10 06:01

I don't like using the concept of "actions" in the route of an ASP.NET Web API. The action in REST is supposed to be the HTTP Verb. I implemented my solution in a somewhat generic and somewhat elegant way by simply using the concept of a parent controller.

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

Below is that answer reproduced in full because I'm not sure what to do when one post answers two SO questions :(


I wanted to handle this in a more general way, instead of wiring up a ChildController directly with controller = "Child", as Abhijit Kadam did. I have several child controllers and didn't want to have to map a specific route for each one, with controller = "ChildX" and controller = "ChildY" over and over.

My WebApiConfig looks like this:

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

My parent controllers are very standard, and match the default route above. A sample child controller looks like this:

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
}

Some drawbacks of my implementation

  • As you can see, I used an enum, so I'm still having to manage parent controllers in two separate places. It could have just as easily been a string parameter, but I wanted to prevent api/crazy-non-existent-parent/5/comment/122 from working.
  • There's probably a way to use reflection or something to do this on the fly without managing it separetly, but this works for me for now.
  • It doesn't support children of children.

There's probably a better solution that's even more general, but like I said, this works for me.

查看更多
登录 后发表回答