Недавний релиз 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 воспоследуют в следующий раз :)человекопонятный