Недавний релиз ASP.Net MVC4 Beta заставил меня внимательно посмотреть на новые возможности и без того замечательного фреймворка. Среди этих возможностей наиболее любопытной мне показалась Web API, которая предоставляет удобный интерфейс создания открытых API для сайтов/сервисов, для использования их из различных клиентов (javascript, мобильные приложения, b2b-сервисы, а то и вовсе desktop-клиенты :)).
Заодно я решил слегка обновить свои знания о вариантах создания веб-сервисов на .net в принципе, и вспомнить, что нам на сегодняшний день предлагает WCF. Оказалось, что нового весьма немало.
Исторически Web API приходит в MVC из WCF Web API, принося, помимо удобного rest-синтаксиса методов еще и возможность standalone-запуска без обязательной необходимости в IIS.
MVC4 WebAPI ко всему прочему вместила в себя еще и функционал WCF Data Services, в части поддержки возвращаемого типа IQueryable и составления запросов на стороне клиента.
Допустим, у нас есть некий набор данных, который необходимо «открыть» через сервис, например — список Документов (открывать его будем в виде IQueryable<Document>). Как это было в WCF DataServices:
public class DataProvider
{
public IQueryable<Document> Documents
{
get
{
return (new[]
{
new Document() {Info = "a", Title = "qwe1"},
new Document() {Info = "b", Title = "qwe2"},
}).AsQueryable();
}
}
}
public class DataService2 : DataService<DataProvider>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
Мы создаем DataProvider, который включает в себя свойства, возвращающие IQueryable. После этого WCF-сервис, раскрывающий эти данные, создается «в одну строчку» — наследованием от базового класса DataService с указанием нашего DataProvider в качестве generic-аргумента.
Что получаем на выходе? Во-первых, традиционный для WCF способ работы с сервисами через SOAP: Add Service Reference в клиентском проекте на VS и вуа-ля:
var client = new WcfWebApi.DataProvider(new Uri("http://localhost:44087/DataService2.svc/"));
var docs = client.Documents.Where(x => x.Info == "a").ToList();
Фильтрация при этом происходит, естественно, на стороне сервера. Однако это не единственный способ работы с WCF WebAPI (до MVC WebAPI мы еще даже не дошли :))! Тот же самый запрос можно послать прямо из браузера! Открываем адрес: «http://localhost:44087/DataService2.svc/Documents?$filter=Info eq ‘a’«, получаем ответ:
<feed>
<entry>
<content type="application/xml">
<m:properties>
<d:ID m:type="Edm.Int32">0</d:ID>
<d:Title>qwe1</d:Title>
<d:Info>a</d:Info>
</m:properties>
</content>
</entry>
</feed>
Конечно, формат ответа не самый удобный для чтения (и это я еще опустил ненужные детали :)), но тем не менее — «человекопонятный» URL делает принципиально возможным работу с таким сервисом и без использования SOAP-клиентов (например, из javascript). Эту возможность (выборка/сортировка через параметры запроса) даёт нам протокол OData, разработанный Microsoft, но переведенный в статус открытого. Возможности протокола очень широки, детальнее с ними ознакомиться можно на странице параметров URI, а я лишь приведу валидный OData-адрес для повышения любопытства: /Products?$filter=Price le 200 and Price gt 3.5&$orderby=Price desc&$skip=5&top=5. Несложно догадаться, что именно делает этот запрос :)
Протокол OData полноценно поддерживается WCF DataServices, а вот WebAPI — лишь частично. Однако базовые операции вполне работают (не работает, например, $select).
К слову, при совместном использовании DataService и EntityFramework можно достаточно просто и удобно «открывать наружу» и операции добавления и изменения БД-сущностей. В обход EF добавление и обновление реализовать тоже можно, но геморрой по написанию провайдеров ожидает приличный :) Эта теоретическая возможность сказывается минусом и в некоторой захламленности клиентского API к DataService (размер скролла как-бы намекает на количество функций :)):

Что же предлагает нам MVC4 WebAPI? Ну, почти то же самое :)
public class DocumentsController : ApiController
{
// GET /api/documents
public IQueryable<Document> Get()
{
return (new[] {
new Document() { Info = "a", Title = "qwe1" },
new Document() { Info = "b", Title = "qwe2" }, }).AsQueryable();
}
// GET /api/documents/popular
public IQueryable<Document> Popular()
{
return (new[] {
new Document { Info = "a", Title = "qwe3" },
new Document { Info = "b", Title = "qwe4" }, }).AsQueryable();
}
// GET /api/documents/5
public Document Get(int id)
{
return new Document { Info = "a", Title = "qwe3" };
}
// POST /api/documents
public void Post(string value)
{
}
// PUT /api/documents/5
public void Put(int id, string value)
{
}
// DELETE /api/documents/5
public void Delete(int id)
{
}
}
Собственно, код примера рассказывает почти обо всём :) В ASP.Net MVC4 появился новый базовый тип для контроллеров — ApiController (он, к слову, не наследуется от привычных ранее по MVC3 Controller или ControllerBase!), при наследовании от него мы получаем удобную возможность создавать различные обработчики/экшены в зависимости от HTTP-метода, которым был сделан запрос. Согласно конвенциям, для удаления используется http-метод DELETE, для обновления — PUT, для добавления — POST, для чтения — GET.
Всё обычно, просто и очевидно, и написание этих обработчиков (в отличие от WCF Data Service) проблем не представляет. Плюс, мы получаем всю полноту и мощь возможностей ASP.Net MVC — это и валидация, и инъекция зависимостей, и вся-вся-вся инфраструктура. Ну и самое главное — ответ на GET-запросы! Даже просто открыв адрес «http://localhost:44087/api/Documents/» в браузере мы получаем куда более понятный xml:
<ArrayOfDocument xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Document>
<ID>0</ID>
<Title>qwe1</Title>
<Info>a</Info>
</Document>
<Document>
<ID>0</ID>
<Title>qwe2</Title>
<Info>b</Info>
</Document>
</ArrayOfDocument>
А при запросе из javascript мы и вовсем получим в ответе любимый всеми javascript-разработчиками JSON:
[{"ID":0,"Info":"a","Title":"qwe1"},{"ID":0,"Info":"b","Title":"qwe2"}]
MVC4 анализирует заголовок Accept запроса, и форматирует ответ в соответствии с ним (для получения JSON должен быть указан Accept: application/json).
Настройка WebAPI сводится к добавлению в Global.asax следующего маршрута (он будет добавлен по умолчанию при создании WebAPI проекта):
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Как видно, в этом маршруте отсутствует имя action’a, определение вызываемого метода идет исключительно по типу http-запроса. Иногда однако, как в примере выше, хочется сделать несколько GET-методов, возвращающих разные наборы:
// GET /api/documents - возврат всех документов // GET /api/documents/popular - возврат популярных документов // GET /api/documents/recent - возврат последних добавленных документов
Для этого маршрут нужно немного подправить:
routes.MapHttpRoute(
name: "DefaultApi2",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional, action = "Get" }
);
Если документов в базе у вас более 9000 и нет желания по запросу /api/documents отдавать все сразу, то можно ограничить максимальное количество возвращаемых строк атрибутом ResultLimit:
// GET /api/documents/limited
[ResultLimit(10)]
public IQueryable<Document> Popular()
{
return Documents;
}
..длина заметки начинает меня пугать, поэтому детальная остановка на вариантах использования и примерах клиентских реализаций ASP.Net MVC4 WebAPI воспоследуют в следующий раз :)человекопонятный