Pull to refresh

RESTFul Api контроллеры в .NET MVC 4

Reading time 5 min
Views 99K
Приветствую.
Летом вышел релиз новой версии фреймворка, но поработать с ним получилось только недавно. В новой версии было добавлено много полезных штук, об одной из них, а именно 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 MethodPUT и Content typeapplication/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
Tags:
Hubs:
+15
Comments 57
Comments Comments 57

Articles