Недавний релиз 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 и вуа-ля:
1 2 | 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’«, получаем ответ:
1 2 3 4 5 6 7 8 9 10 11 | < 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? Ну, почти то же самое :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 | < 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:
1 | [{ "ID" :0, "Info" : "a" , "Title" : "qwe1" },{ "ID" :0, "Info" : "b" , "Title" : "qwe2" }] |
MVC4 анализирует заголовок Accept запроса, и форматирует ответ в соответствии с ним (для получения JSON должен быть указан Accept: application/json).
Настройка WebAPI сводится к добавлению в Global.asax следующего маршрута (он будет добавлен по умолчанию при создании WebAPI проекта):
1 2 3 4 5 | routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{id}" , defaults: new { id = RouteParameter.Optional } ); |
Как видно, в этом маршруте отсутствует имя action’a, определение вызываемого метода идет исключительно по типу http-запроса. Иногда однако, как в примере выше, хочется сделать несколько GET-методов, возвращающих разные наборы:
1 2 3 | // GET /api/documents - возврат всех документов // GET /api/documents/popular - возврат популярных документов // GET /api/documents/recent - возврат последних добавленных документов |
Для этого маршрут нужно немного подправить:
1 2 3 4 5 | routes.MapHttpRoute( name: "DefaultApi2" , routeTemplate: "api/{controller}/{action}/{id}" , defaults: new { id = RouteParameter.Optional, action = "Get" } ); |
Если документов в базе у вас более 9000 и нет желания по запросу /api/documents отдавать все сразу, то можно ограничить максимальное количество возвращаемых строк атрибутом ResultLimit:
1 2 3 4 5 6 | // GET /api/documents/limited [ResultLimit(10)] public IQueryable<Document> Popular() { return Documents; } |
..длина заметки начинает меня пугать, поэтому детальная остановка на вариантах использования и примерах клиентских реализаций ASP.Net MVC4 WebAPI воспоследуют в следующий раз :)человекопонятный