Приветствую.
Летом вышел релиз новой версии фреймворка, но поработать с ним получилось только недавно. В новой версии было добавлено много полезных штук, об одной из них, а именно ApiController, я хотел бы сегодня рассказать.
Благодаря им стало возможно делать RESTFull Api без лишних усилий. На небольшом примере заодно разберем работу с OData.
Создадим новый ASP MVC 4 Empty Project. Для примера, создадим контроллер, который будет реализовывать функционал по работе с топиками. Для начала добавим простую модель:
public class Topic
{
public int Id { get; set; }
public string Title { get; set; }
}
Добавим новый контроллер, унаследуем его от ApiController, пока без никаких действий:
public class TopicController : ApiController
{
}
Теперь наш контроллер доступен по адресу: localhost/api/topic. Если мы перейдем по нему, то получим сообщение о том, что в нашем контроллере не найдено ни одного действия, реализующего ответ на GET запрос. Так добавим же его в наш контроллер:
public class TopicController : ApiController
{
public ICollection<Topic> Get()
{
return new Collection<Topic>
{
new Topic { Id = 1, Title = "Топик 1"},
new Topic { Id = 2, Title = "Топик 2"}
};
}
}
Если мы сделаем запрос на localhost/api/topic, то получим следующий ответ:
<ArrayOfTopicModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ApiControllerTutorial.Models">
<TopicModel>
<Id>1</Id>
<Title>Топик 1</Title>
</TopicModel>
<TopicModel>
<Id>2</Id>
<Title>Топик 2</Title>
</TopicModel>
</ArrayOfTopicModel>
Почему ответ в формате XML? Потому, что если мы не указали Content-Type в запросе, сериализатор по умолчанию вернет нам XML. Давайте получим в формате JSON. Для этого можно воспользоваться удобным приложением для Chromium — REST Console (за подсказку подобных плагинов/расширений для других браузеров буду благодарен). Укажем в Content-Type «json» и получим:
[{"Id":1,"Title":"Топик 1"},{"Id":2,"Title":"Топик 2"}]
Коллекцию топиков мы получили. Добавим новое действие в контроллер для получение одного топика по его идентификатору:
public Topic Get(int id)
{
return new Topic
{
Id = id,
Title = String.Format("Топик {0}", id)
};
}
Запрос по адресу localhost/api/topic/5 вернет нам следующий ответ:
{"Id":5,"Title":"Топик 5"}
Добавим действие для добавления нашего топика:
public string Put(Topic model)
{
return String.Format("Топик '{0}' создан!", model.Title);
}
И отправим по адресу localhost/api/topic/ следующий запрос:
{'Title':'Новый топик'}
Также в запросе укажем необходимые параметры: Request Method — PUT и Content type — application/json (Не перепутайте этот Content type с тем, о котором я говорил выше. Этот указывается в Content Headers, чтобы байндер знал, в каком формате пришли к нему данные, а для запроса топиков мы указывали Content type в Accept для сериализатора). И получим в ответ:
"Топик 'Новый топик' создан!"
Кстати о возвращаемом значении. В нашем случае я вернул строку с сообщением. Также можно возвращать HttpResponseMessage и манипулировать кодами ответа, в зависимости от успеха/неудачи операции:
public HttpResponseMessage Put(Topic model)
{
if(String.IsNullOrEmpty(model.Title))
return new HttpResponseMessage(HttpStatusCode.BadRequest);
/*Логика сохранения*/
return new HttpResponseMessage(HttpStatusCode.Created);
}
Действие для метода POST описывать не буду, т.к. отличий от PUT — нет. Добавим последнее действие DELETE:
public string Delete(int id)
{
return String.Format("Топик {0} удален!", id);
}
Роутинг
А что, если мы захотим использовать наш контроллер для предоставления простого апи, без поддержки методов?
Добавим новый контроллер с двумя действиями:
public class TestRouteController : ApiController
{
public string GetTopic(int id)
{
return String.Format("Topic {0}", id);
}
public string GetComment(int id)
{
return String.Format("Comment {0}", id);
}
}
Если мы отправим запрос на localhost/api/testroute/5, то получим ошибку: Multiple actions were found that match the request. Связано это с тем, что Selector не знает какое действие ему выбрать. Давайте откроем WebApiConfig.cs и посмотрим на заданный там маршрут:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Как видим, у нас не задано в шаблоне определение действия для контроллера. Необходимое действие контроллера выбирается на основе Method'a запроса. Добавим ниже еще один маршрут:
config.Routes.MapHttpRoute(
name: "DefaultApiWithAction",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
После этого, если сделать запрос к нашему контроллеру с прямым указанием необходимого действия (localhost/api/testroute/gettopic/5), то мы получим ответ. Либо можно задать у самого действия необходимый маршрут для него с помощью атрибута ActionName:
[ActionName("topic")]
public string GetTopic(int id)
{
return String.Format("Topic {0}", id);
}
Теперь можно запрашивать localhost/api/testroute/topic/5
OData
Как вы уже обратили внимание, действия контроллером могут возвращать произвольные объекты либо коллекции объектов и они будут успешно сериализованы. Раньше нам необходимо было возвращать ActionResult, и перед этим вручную сериализовывать наши данные. В связи с этим открывается еще одна интересная возможность. Сначала установим OData (Nuget) с помощью пакетного менеджера:
PM> Install-Package Microsoft.AspNet.WebApi.OData -Pre
Добавим новый контроллер:
public class OdataController : ApiController
{
[Queryable]
public IQueryable<Topic> Get()
{
return new
EnumerableQuery<Topic>(
new Collection<Topic>
{
new Topic{ Id = 1, Title = "1"},
new Topic{ Id = 2, Title = "2"},
new Topic{ Id = 3, Title = "3"},
new Topic{ Id = 4, Title = "4"},
new Topic{ Id = 5, Title = "5"}
});
}
}
Сделаем запрос на localhost/api/odata/:
[{"Id":1,"Title":"1"},{"Id":2,"Title":"2"},{"Id":3,"Title":"3"},{"Id":4,"Title":"4"},{"Id":5,"Title":"5"}]
Ничего удивительного не произошло. Но посмотрим внимательнее на метод Get нашего контроллера. Он возвращает IQueryable и помечен атрибутом [Queryable], а это значит, что можно применять дополнительные запросы к нашей коллекции с помощью OData прямо в запросе. Сделаем несколько запросов с различными параметрами и посмотрим на ответ:
Запрос | Ответ |
---|---|
localhost/api/odata/ | [{«Id»:1,«Title»:«1»}, {«Id»:2,«Title»:«2»}, {«Id»:3,«Title»:«3»}, {«Id»:4,«Title»:«4»}, {«Id»:5,«Title»:«5»}] |
localhost/api/odata/?$skip=2 | [{«Id»:3,«Title»:«3»}, {«Id»:4,«Title»:«4»}, {«Id»:5,«Title»:«5»}] |
localhost/api/odata/?$skip=1&$top=2 | [{«Id»:2,«Title»:«2»}, {«Id»:3,«Title»:«3»}] |
localhost/api/odata/?$filter=(Id gt 1) and (Id lt 5) | [{«Id»:2,«Title»:«2»}, {«Id»:3,«Title»:«3»}, {«Id»:4,«Title»:«4»}] |
localhost/api/odata/?$filter=(Id gt 1) and (Id lt 5)&$orderby=Id desc | [{«Id»:4,«Title»:«4»}, {«Id»:3,«Title»:«3»}, {«Id»:2,«Title»:«2»}] |
Магия, не правда ли?
Отправка форм контроллерам: http://www.asp.net/web-api/overview/working-with-http/sending-html-form-data,-part-1#sending_complex_types
Все про OData: http://msdn.microsoft.com/en-us/library/ff478141.aspx
Архив с проектом: yadi.sk/d/k2KaG0cL1fXhA
Автор: vyacheslav_ka