Вложенные ресурсы в ASP.net MVC 4 WebApi
Есть ли лучший способ в новом ASP.NET MVC 4 WebApi обрабатывать вложенные ресурсы, чем настраивать специальный маршрут для каждого из них? (аналогично здесь: Поддержка ASP.Net MVC для вложенных ресурсов? - это было опубликовано в 2009 году).
Например, я хочу обработать:
/customers/1/products/10/
Я видел несколько примеров ApiController
действий, отличных от Get()
, Post()
и т.д., например здесь Я вижу пример действия под названием GetOrder()
. Однако я не могу найти документацию. Это способ достичь этого?
Ответы
Ответ 1
Извините, я обновил это несколько раз, так как сам я нахожу решение.
Кажется, есть много способов решить эту проблему, но наиболее эффективным я нашел до сих пор:
Добавьте это по умолчанию:
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]
и т.д.
Ответ 2
EDIT:. Хотя этот ответ по-прежнему применяется для Web API 1, для Web API 2 я настоятельно рекомендую использовать ответ Daniel Halan, поскольку он современное состояние для отображения подресурсов (среди других тонкостей).
Некоторые люди не любят использовать {действие} в веб-API, потому что считают, что при этом они будут нарушать идеологию 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, }
);
Вы можете обрабатывать их в CustomerController следующим образом:
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
}
// ...
}
Вы также можете направить действие Create на один и тот же "ресурс" (заказы):
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)
}
// ...
}
Да, вам все равно придется создавать множество маршрутов только потому, что веб-API по-прежнему не может маршрутизировать различные методы в зависимости от пути... Но я думаю, что более чисто декларативно определять маршруты, чем придумывать настраиваемые механизмы диспетчеризации на основе перечислений или других трюков.
Для потребителя вашего API он будет выглядеть отлично RESTful:
GET http://your.api/customers/1/orders
(карты GetOrders (длинный), возвращающий все заказы для клиента 1)
GET http://your.api/customers/1/orders/22
(отображает GetOrders (длинный, длинный), возвращающий заказ 22 для клиента 1
POST http://your.api/customers/1/orders
(отображается в CreateOrder (длинный), который создаст заказ и вернет его вызывающему абоненту (с новым только что созданным ID)
Но не принимайте слово как абсолютную истину. Я все еще экспериментирую с этим, и я думаю, что MS не справилась с надлежащим доступом к подресурсам.
Я призываю вас попробовать http://www.servicestack.net/ для менее болезненного опыта написания REST apis... Но не поймите меня неправильно, Я обожаю Web API и использую его для большинства моих профессиональных проектов, в основном потому, что легче найти программистов там, которые уже "знают" его... Для моих личных проектов я предпочитаю ServiceStack.
Ответ 3
Так как 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();
Ответ 4
Мне не нравится использовать концепцию "действия" на пути веб-API ASP.NET. Действие в REST должно быть HTTP-глаголом. Я реализовал свое решение несколько шире и несколько элегантно, просто используя концепцию родительского контроллера.
fooobar.com/info/150885/...
Ниже приведен ответ, воспроизведенный полностью, потому что я не уверен, что делать, когда одно сообщение отвечает на два вопроса: (
Я хотел бы обработать это более общим способом, вместо того, чтобы подключать 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
}
Некоторые недостатки моей реализации
- Как вы можете видеть, я использовал
enum
, поэтому мне все еще нужно управлять родительскими контроллерами в двух разных местах. Он мог бы так же легко быть строковым параметром, но я хотел предотвратить работу api/crazy-non-existent-parent/5/comment/122
.
- Вероятно, есть способ использовать отражение или что-то сделать это на лету, не управляя им отдельно, но это работает для меня пока.
- Он не поддерживает детей детей.
Вероятно, лучшее решение, которое еще более общее, но, как я уже сказал, это работает для меня.