Skip to content
May 9, 2013 / ahriman hpc mode

Web API и Windows Azure. Часть 1. История, немного про REST, примеры.

Привет.

Начинаю новый цикл статей, на этот раз о Web API и приложении этого дела к Windows Azure. Статей будет неизвестно сколько, может, две, может десять, поэтому по мере наполнения буду добавлять всё в оглавление и потом, как обычно, причешу и соберу в PDF.

В этой части:

* История

* Зачем нужен Web API, если есть WCF, например

* Концепции REST и модель RMM

* Пример первого приложения ASP.NET MVC + Web API + jQuery + AJAX

* Использование OData в Web API

Итак, для того, чтобы понять, зачем всё это нужно, необходимо определиться с объектом изучения и его предназначением. Но сначала немного истории, чтобы было понятнее, почему (по моему мнению) была введена новая сущность в лице Web API.

История

       Не углубляясь в подробности того, как было, достаточно сообщить, что Microsoft прошла долгий путь в вопросах разработки сервисоориентированных систем, который оброс несколькими основными столпами: ASMX, .NET Remoting, WCF. Используя веб-сервисы на основе ASMX (это ~2002 год), разработчики могли довольно просто создать веб-сервис, который реализовывал различные версии SOAP, но был доступен только поверх HTTP. Параллельно существовала технология Remoting, которая давала возможность разрабатывать сервисы, которые могли использовать не только HTTP. В 2006 году Microsoft выпустила .NET 3.0 и Windows Communication Foundation (WCF), которая разом покрывала то, что предлагали ASMX и .NET Remoting, и предлагала еще больше новых возможностей, включая поддержку новых стандартов. Например, с WCF можно разработать веб-сервис для TCP, с поддержкой аутентификации по токенам безопасности, и запустить его в собственноручно разработанном сервисе Windows. Лично мое знакомство с WCF началось в 2009 году, когда я написал небольшую оснастку для сбора логов с двух десятков машин в кластере – на головной машине был запущен WCF-сервис, остальные же машины выполняли раз в N минут скрипт на Powershell, который вызывал WCF-сервис и передавал ему данные. Не скажу, что это было совсем просто делать с первого раза, но сама элегантность подхода мне очень понравилась.

    В 2007 году в версии CTP был выпущен фреймворк ASP.NET MVC. Уже тогда витала идея о том, что Интернет используется довольно просто – для обмена информацией между веб-страницами используются чаще всего текстовые сообщения, а не бинарные данные, используется HTTP, клиентская часть для обработки различных задач. Это привело к идее разработки того, что имело бы поддержку сериализации только в XML и JSON. Зачем, собственно, мне, если я хочу создать простейший RESTful сервис, использовать в целом местами тяжеловесный WCF, который, тем более, не особо ложился на REST?  Вот тут и вступил в игру ASP.NET MVC со своим встроенным механизмом маршрутизации.

Маршрутизация MVC

В отличие от WCF, где сервис = адрес физического файла, механизм маршрутизации в MVC работает по иному принципу – он ассоциирует адрес с методом контроллера.

Если рассматривать пример, то, если бы имели сервис WCF, то обращение к нему происходило бы так:

http://server/MyService.svc

Если ASP.NET MVC, то так:

http://server/MyService/Get/123

Как видите, подобная адресация позволяет скрыть детали внутренней реализации. С помощью маршрутизации MVC мы можем перевести запрос по вышеупомянутому адресу на любой метод любого контроллера. Все это приводит к гораздо более простой поддержке приложения – мы можем менять внутреннюю реализацию так, как представляется удобным, и, например, быстро менять старую версию реализации на новую, всего лишь подправив конфигурацию маршрутизации. Создание же Web API (в общем-то, "нашлепка" на ASP.NET) позволило разрабатывать с использованием этого механизма маршрутизации HTTP REST-сервисы, которые работают в одном проекте с ASP.NET MVC и, в общем-то, полностью интегрируются в контекст (не имплементации, а логический) MVC. Но, перед тем, как начать писать REST-сервис с Web API, нужно определиться с тем, что я называю REST-сервисом. Об этом – ниже.

Немного про REST

Многие думают, что если сделать линки красивыми, то их API уже будет 100% REST. На самом деле это, конечно же, не так, и REST, упомянутый впервые в диссертации одного умнейшего человека, не так прост, как кажется. Правила построения REST-сервисов диктуют также и ограничения, которые накладываются далее на архитектуру, и это надо учитывать и соглашаться (или не соглашаться).

Для того, чтобы понять, что под собой подразумевает путь к REST, можно воспользоваться моделью RMM (REST Maturity Model), представленной Леонардом Ричардсоном в 2008 году. Те, кто знаком с CMMI (Capability Maturity Model Integration), сразу увидят сходство в словах – оно неслучайно. Эта модель, как и CMMI, описывает несколько уровней соответствия некоторому своду правилу, методологии. RMM содержит 4 уровня –  от 0 до 3. В этой модели все начинается с самого простого на уровне 0, когда API соответствует стилю RPC, и заканчивается удовлетворяющим всем основным парадигмам REST. Разумеется, что, если использовать эту модель для описывания собственной системы, если засели на каком-то уровне, меньшем последнего – этот сервис не REST. Давайте сначала посмотрим на не очень красивую картинку, схематично изображающую RMM, после чего перейдем к примеру и разберем его согласно всем уровням RMM. Примером нам будет служить сервис гостевой книги.

image

Уровень 0

Согласно уровню 0, у нас был бы простой WCF-сервис GuestService, у которого был бы один метод CreatePost(), который принимал бы аргументы заголовка записи, имени пользователя и его e-mail. В ответ метод возвращал бы номер записи. Специально для администратора было бы еще несколько методов – DeletePost() и UpdatePost(). Каждый из них принимал бы какое-то сообщение и возвращал какой-то ответ. И, конечно же, GetAllPosts(). Обычная система, ничего примечательного.

Таким образом, наш сервис на уровне 0 RMM выглядел бы так:

Метод URL HTTP-метод Видимость
CreatePost /api/GuestService.svc POST Например, WSDL
DeletePost /api/GuestService.svc POST Например, WSDL
UpdatePost /api/GuestService.svc POST Например, WSDL
GetAllPosts /api/GuestService.svc POST Например, WSDL

Согласно таблице, у нас есть некоторое API, каждая из операций которого имеет одну и ту же ссылку (URL). В целом же ссылка выглядит совершенно непонятно и непривязано к контексту – удаляем мы запись с номером 1, или обновляем пост с номером 100 – URL будет тот же самый. Эта система удовлетворяет уровню 0 RMM и основным характеристикам – один URL = один HTTP-метод. Если смотреть на HTTP-методы, то тут все тоже одинаково – везде POST, да еще и методы самому приходится внутри создавать. Одно из правил RESTful HTTP – не надо создавать самому какие-то методы. Надо соответствовать тому списку, который содержит доступные HTTP verbs. Но об этом чуть позже.

Да, и, конечно, в нашей ситуации с данным сервисом клиент должен каким-то образом знать о том, как вызвать то или иное действие, то есть должна наличествовать определенная связь, например, клиент должен знать о контрактах (вот он, WSDL). Это тоже нехорошо – лишние связи – зачем они нам? Тем более что REST гласит, что нужно знать только основной адрес, все остальные операции должны быть доступны через так называемые гипермедиа-элементы (ссылки, формы, элементы управления). Сервер должен управлять всем процессом – диктовать то, как должны выглядеть ссылки, формы, забирая эти знания на себя и не делясь им с клиентом, чтобы, если что-то изменилось, быстро отреагировать на эти изменения. Это все – компоненты и части принципа HATEOAS, о котором позже. Пока же перейдем на уровень 1.

Уровень 1

Итак, мы захотели превратить наш сервис в REST-сервис. Что же мы должны сделать? Во-первых, удовлетворить правилам уровня 1. Правила уровня 1 гласят, что REST-сервис должен быть ресурсоориентированным, то есть ориентироваться не на запрос-ответ и методы, как в RPC, а на ресурсы, ограничиваемые набором HTTP verbs. И всё. Есть набор доступных HTTP verbs – больше ничего не надо. GET, DELETE, POST, иногда PUT и еще более иногда HEAD и иже с ними. Этот принцип основополагающ для REST-сервисов – если у нас сервис предлагает большую пачку различных методов, он не будет REST (согласно RMM).

Таким образом, следуя правилу уровня 1 “Много URL, один HTTP метод”, построим новую табличку для API.

Метод URL HTTP-метод Видимость
CreatePost /api/posts POST Например, WSDL
DeletePost /api/posts/1 POST Например, WSDL
UpdatePost /api/posts/1 POST Например, WSDL
GetAllPosts /api/posts POST Например, WSDL

Но и тут образовывается проблема RESTful-ности – клиент продолжает не представлять о том, чем отличаются ссылки /api/posts и /api/posts – без контракта и, например, WSDL. Клиент должен иметь контракт. Связность продолжает наблюдаться, и, кроме этого, продолжает наблюдаться один HTTP-метод. Но, по крайней мере, наш сервис перебрался на 1 уровень модели RMM. Попробуем перейти на 2.

Уровень 2

Правило уровня 2 = “Много URL = Много HTTP-методов”. Здесь в полной мере надо переосмыслить свой сервис в сторону ресурсоориентированности. Что приходит от клиента? Как это обрабатывать? Куда посылать?

По факту, конечно, нет такого HTTP-метода, как CreatePost, и GetAllPosts тоже нет.  Но есть POST и GET! И PUT и DELETE тоже. Мы можем ассоциировать наши методы, возможно, кое-что переименовав, и перейти на уровень 2. Здесь нужно уточнить, что HTTP-методы имеют некоторые особенности, например, PUT и DELETE идемпотентны. Это значит, что сколько бы мы не вызвали эти методы, они возвратят один и тот же результат- HTTP-ответ – например, сколько бы мы не вызывали PUT, будет изменяться одна и та же сущность. POST же не идемпотентен по понятной причине – создается новая сущность. GET не идемпотентен, но безопасен – в системе ничего не меняется и не должно меняться при любом количестве вызовов GET. Это очень важно, так как я встречал одну систему, в которой в методе GET на стороне сервера происходила логика изменения состояния сущностей в источнике данных. Это неправильно.

Посмотрим теперь, что произошло с нашим сервисом на уровне 2.

Метод URL HTTP-метод Видимость
CreatePost /api/posts POST Например, WSDL
DeletePost /api/posts/1 DELETE Например, WSDL
UpdatePost /api/posts/1 PUT Например, WSDL
GetPost /api/posts GET Например, WSDL

Уже лучше. Теперь у нас сервис, который имеет набор соответствующих HTTP-методов и различные URL (по большей части). Но мы всё ещё связаны с клиентом. Чтобы решить это, пойдем на уровень 3.

Уровень 3

Уровень 3 характеризуется одной емкой аббревиатурой – HATEOAS. Посыл этой аббревиатуры заключается в том, что клиент не должен знать ничего, кроме начального адреса сайта. Все остальное ему подскажет сам сайт – с помощью ссылок, кнопок, элементов управления. Клиент должен получать доступ к ресурсам и их управлению без предварительных знаний о том, как это сделать.

Нарушить связность можно, предоставив возможность клиенту менять состояние, используя то, что возвращает сервис – например, если клиент перешел на определенный адрес, ему может быть возвращены соответствующие данные. В любом случае, создатель сервиса должен быть уверен, что при нажатии на кнопку Submit веб-формы будет инициирован POST-запрос и вызван соответствующий метод.

Итак, удовлетворяющее 3 уровню API приведено в таблице. Хотелось бы обратить внимание, что, несмотря на всё это, даже если сервис удовлетворяет всем условиям RMM, могут быть определенные нюансы, которые нарушат всё.

Метод URL HTTP-метод Видимость
CreatePost /api/posts POST HTTP POST
DeletePost /api/posts/1 DELETE HTTP DELETE
UpdatePost /api/posts/1 PUT HTTP PUT
GetPost /api/posts GET HTTP GET

И последнее про REST

И последнее про REST – если удовлетворять стандартам, образовавшимся вокруг HTTP, то нельзя забывать про коды HTTP. Сервис должен возвращать известные коды ошибок. Это необязательное условие, но, если эти коды есть, значит, их придумали и утвердили умные люди, и желательно использовать их, чтобы тот, кто будет поддерживать сервис, не увидел ничего, что нарушило бы его уверенность в том, что автор сервиса писал код в сознательном состоянии.

Ниже приведена таблица с извесными кодами HTTP.

Код статуса Значение
200 ОК, запрос выполнен успешно.
201 Запись создана – может включать ссылку на созданный ресурс
202 То же самое, что и 200, но используется в связке с асинхронной операцией.
301 Запрашиваемый ресурс был перемещен – нужно включать в сообщение URL на новое расположение ресурса
400 Плохо оформленный запрос.
401 Unauthorized – клиенту не разрешен доступ к ресурсу.
403 Доступ запрещен – клиент аутентифицировался, но не прошел авторизацию.
404 Ресурс не найден либо клиент не имеет доступа и не должен знать, почему.
409 Конфликт на сервере – используется в ответ на PUT, когда несколько клиентов используют один ресурс.
500 Ошибка на сервере.

Web API

Напомню, что Web API появился в ASP.NET MVC 4 и сразу же вызвал достаточно много разговоров о себе. Очередная технология для помощи в создании веб-сервисов, опять REST, зачем нам это нужно? Это хороший вопрос – ведь есть всякие WCF и иже с ним.  Но всё это вещи, если можно так выразиться, глобальные, и, когда нужно реализовывать какой-то небольшой сценарий, то надо тянуть за собой еще немного того, что не нужно и неизвестно, пригодится ли. Web API – это как раз то средство, которое позволяет очень просто, быстро и красиво реализовать сценарий RESTful-сервиса с использованием HTTP.

Создадим ASP.NET MVC 4 приложение и выберем соответствующий Web API шаблон.

image

image

После того, как проект будет создан, вы увидите обыкновенное MVC-приложение. Первой точкой входа будет новый контроллер – ValuesController, который является специализированным контроллером WebAPI и наследуется, в свою очередь, не от Controller, а от ApiController.

Как и в любом другом контроллере, scaffolding уже создает некоторый код, в случае Web API-контроллера это "болванки" для методов, замыкающих на себя HTTP verbs – GET, POST, PUT и DELETE.

HTTP Verb Метод контроллера Описание
Get() GET Метод возвращает набор данных в виде массива. Может быть возвращен любой тип коллекции IEnumerable.
Get(string) GET Метод возвращает одну сущность, которая связана с тем, что передается в виде string (часто это ID).
Post(string) POST Метод добавляет новую сущность в систему.
Put(string,string) PUT Метод обновляет существующую сущность. Различие между Put и Post состоит в том, что Post всегда создает новую сущность, Put – нет.
Delete(string) DELETE Метод удаляет сущность из системы.

Теперь добавим контекст для тестовой базы данных. В данном цикле статей я буду использовать тестовую базу данных, которую всегда использую, когда веду курсы в http://atraining.ru. Она состоит всего из двух очень простых несвязанных таблиц – CourseSet и Student. В дальнейшем, может, и усложнится, но это непринципиально для обсуждаемой темы.

image

 

Внесем изменения в контроллер WebAPI, определив соответствующим образом поведение его методов. Так как код очень простой и понятный, приведу его без комментариев.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web.Http;
using WebApiJsonAjax.Models;
namespace WebApiJsonAjax.Controllers
{
    public class ValuesController : ApiController
    {
        private atrainingdatabaseEntities _ctx = new atrainingdatabaseEntities();
        // GET api/values
        public List<Models.Student> Get()
        { 
            
              var students = from e in _ctx.Student select e;
              return students.ToList(); 
     
             
        }

       
        // GET api/values/5
        public string Get(int id)
        {
            return "value" + id;
        }

        public Student Post(Student student)
        {
            return student;
        }

        // PUT api/values/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        public void Delete(int id)
        { 
            var student = _ctx.Student.Where(s => s.id == id).FirstOrDefault();
_ctx.Student.Remove(student);
_ctx.SaveChanges(); 
            
        }
    }
}

Обращу внимание, что я не просто так где-то использую нотацию LINQ в виде расширений и лямбд, а где-то – в ином виде. Просто чтобы запомнить. На самом деле мне кажется, что нотация LINQ в виде расширений гораздо более понятна, несмотря на активное использование лямбд, но – на вкус и цвет.

Сохраним контроллер и попробуем протестировать то, что получилось. Нажмем F5 и перейдите на ссылку http://localhost:[port]/api/values . Обратим внимание, что при обращении с GET-запросом к этой ссылке будет выдан результат в виде JSON. WebAPI умеет работать и возвращать значения в виде JSON и XML, это зависит от того, что клиент понимает. Посмотрим, что он возвратит в Chrome и IE (скриншоты расположены соответственно), чтобы подойти к еще одной замечательной особенности Web API под названием Content Negotiation.

image

image

Весь процесс сериализации в JSON объекта типа Student взял на себя фреймворк, что не может не радовать – в отличие, например, от использования ASP.NET MVC, когда для того, чтобы возвратить JSON из контроллера, нужно использовать return Json явным образом. Давайте рассмотрим, почему же в двух браузерах сервис возвратил результат в разных форматах.

Content Negotiation

В стандарте HTTP есть такое понятие, как Content Negotiation, которое означает процесс согласования между клиентом и сервером деталей относительно их коммуникаций. Когда клиент обращается к серверу, он отправляет в запросе директиву Accept, в которой пишет, что и в какой мере он ожидает увидеть от сервера в ответе. Попробуем выполнить тот же самый запрос GET, но с помощью утилиты curl – с ее помощью можно довольно просто отслеживать, что происходит во время запроса. Дополнительно будут скриншоты из Fiddler. Также посмотрим запросы, которые отправляются Chrome и IE, с помощью интегрированных в сами браузеры средств.

image

 

image

 

Интерфейс Fiddler, с которым мы будем также работать параллельно curl.

image

 

Значение Accept – это то, что хотел бы увидеть клиент от сервера. Как видно из скриншотов, Chrome хочет видеть text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 (q – это “вес” формата, чем больше вес, тем больше “хочет” клиент увидеть результат именно в этом формате и указывает на это серверу). IE же хочет видеть text/html, application/xhtml+xml, */*. Хочу обратить внимание, что Web API не распознает то, что хочет видеть IE, и отправляет результат по умолчанию в JSON – у него нет поддержки application/xhtml+xml, но есть поддержка */*, то есть “приму всё, что предложит сервер”. Chrome же получает то, что хочет, в желаемом формате XML.

Давайте теперь посмотрим, как можно это сделать в curl и Fiddler и имитировать некоторые аспекты поведения. Обратимся с запросом GET.

image

image

 

Ответ в JSON, так как клиенту все равно, в каком формате придет результат.

Давайте укажем явным образом, что мы хотим увидеть.

image

image

Замечательно – формат ответа зависит от того, чего хочет клиент.

Этот процесс и называется Content Negotiation, и он является очень важным процессом в коммуникациях между клиентом и сервером. Хочу также обратить внимание на то, с какой простотой и элегантностью решен выбор между форматами. Указание же формата результата на сервере сопряжено с несколько более сложным кодом, который приведен ниже.

public HttpResponseMessage Get()
{

    var students = (from e in _ctx.Student select e);

    var resp = new HttpResponseMessage(HttpStatusCode.OK);
    resp.Content = new ObjectContent<IEnumerable<Student>>(students, new JsonMediaTypeFormatter());

    resp.Headers.ConnectionClose = true;
    resp.Headers.CacheControl = new CacheControlHeaderValue();
    resp.Headers.CacheControl.Public = true;

    return resp;

}

Если вы хотите возвратить результат именно в том формате, в котором хотите _вы_, не учитывая клиента, то необходимо получить доступ к сообщению, которое будет формироваться для результата, и определить его свойства, передав его специальному механизму – Formatter. Конечно же, вы можете писать свои конвертеры со сколь угодно сложной логикой.

Итак, в коде выше происходит:

1) Получаются данные из коллекции Student

2) Создается экземпляр класса HttpResponseMessage со статусом 201 OK. Это сообщение будет возвращено клиенту.

3) В содержание сообщения вкладывается наша модель данных, которая конвертируется в формат JSON.

4) Сообщение возвращается клиенту.

Давайте повторим наш запрос в curl, указав, что мы хотим видеть только application/xml.

image

Несмотря на Accept: application/xml, результат был возвращен в формате JSON.

Content Negotiation в Web API во всей своей красоте. А мы двигаемся дальше.

Использование Web API из представления MVC с помощью jQuery

У нас уже есть почти готовый REST-сервис. Но в Web API вполне закономерно не предусмотрен механизм генерации представлений. Свяжем сервис с представлением MVC.

Использовать Web API мы будем из представления Index у HomeController. С помощью jQuery заберем данные из сервиса и нарисуем простую табличку.

Привожу весь код страницы.

<header>
    <div class="content-wrapper">
        <div class="float-left">
            <p class="site-title">
                <a href="~/">ASP.NET Web API</a></p>
        </div>
    </div>
</header>
<div id="body">
    <section class="featured">
        <div class="content-wrapper">
           
            <table id="students"></table>
            <script>
               
                $(function () {
                    var $students = $("#students");
                    $.ajax({
                        url: "api/values",
                        contentType: "json",
                        success: function(data) {
                            $.each(data, function(index, item) {
        var $row = $("#templates").find(".row-template").clone();
        $row.find(".Name").html(item.name);
        $row.find(".delete").click(function() {

        $.ajax({ url: "api/values/" + item.id,
                                        type: "DELETE",
                                        success: function() {
                                            $row.remove();
                                        }
                                    });

                                });
                                $students.append($row);
                               
                            });
                        }
                    });
                })

            </script>
        </div>
    </section>
    <section class="content-wrapper main-content clear-fix">
       
        <div id="templates" style="display:none">
           
            <table>
               
                <tr class="row-template">
                   
                    <td class="Name"></td>
                    <td><input type="button" value ="Del" class="delete"/></td>

                </tr>

            </table>

        </div>

    </section>
</div>

Код достаточно прост – мы получаем из сервиса данные и динамически заполняем таблицу. Ниже приведен скриншот текущего представления

image

. К каждой записи привязана кнопка, которую можно нажать, чтобы удалить запись.

image

Добавим остальную функциональность – PUT и POST. Для этого надо написать достаточно много кода. Приведу полные листинги представления и контроллера с выделенными красным участками измененного кода.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web.Http;
using WebApiJsonAjax.Models;
namespace WebApiJsonAjax.Controllers
{
    public class ValuesController : ApiController
    {
        private atrainingdatabaseEntities _ctx = new atrainingdatabaseEntities();

        public HttpResponseMessage Get()
        {

            var students = (from e in _ctx.Student select e);

            var resp = new HttpResponseMessage(HttpStatusCode.OK);
            resp.Content = new ObjectContent<IEnumerable<Student>>(students, new JsonMediaTypeFormatter());

            resp.Headers.ConnectionClose = true;
            resp.Headers.CacheControl = new CacheControlHeaderValue();
            resp.Headers.CacheControl.Public = true;

            return resp;

        }

        // GET api/values/5
        public string Get(int id)
        {
            return "value" + id;
        }

        public HttpResponseMessage Post([FromBody]Student student)
        {
            try
            {
                _ctx.Student.Add(student);
                _ctx.SaveChanges();
            }
            catch (Exception e)
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message);

            };
            return Request.CreateResponse(HttpStatusCode.OK);
        }

        // PUT api/values/5
        public HttpResponseMessage Put(int id, Student student)
        {
            try
            {
                var studentToChange = _ctx.Student.FirstOrDefault(s => s.id == id);
                studentToChange.name = student.name;
                _ctx.SaveChanges();
            } catch (Exception e)
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e.Message);

            };
            return Request.CreateResponse(HttpStatusCode.OK);
        }

        // DELETE api/values/5
        public void Delete(int id)
        { 
            
                var student = _ctx.Student.Where(s => s.id == id).FirstOrDefault();
                _ctx.Student.Remove(student);
                _ctx.SaveChanges(); 
            
        }
    }
}

В контроллере появилась реализация методов PUT и POST. В обоих методах операции завернуты в блок try/catch и, если что-то не получается, создается ответ, в который вкладывается сообщение об ошибке. Здесь нужно еще упомянуть дополнительный атрибут, который может изрядно помочь в работе – вы можете пометить аргумент метода контроллера как [FromBody] либо [FromUrl], что означает, соответственно, брать значение этого аргумента из тела запроса и из URL.

Код представления ниже. В него добавлены обработчики событий нажатия на кнопки изменения и создания записей. Хочу обратить внимание на использование JSON.stringify – эта функция конвертирует предложенное ей в JSON.

<header>
    <div class="content-wrapper">
        <div class="float-left">
            <p class="site-title">
                <a href="~/">ASP.NET Web API</a></p>
        </div>
    </div>
</header>
<div id="body">
    <section class="featured">
        <div class="content-wrapper">
           
            <table id="students"></table>
            <script>

                $(function() {
                    var $students = $("#students");
                    $.ajax({
                        url: "api/values",
                        contentType: "json",
                        success: function(data) {
                            $.each(data, function(index, item) {
                                var $row = $("#templates").find(".row-template").clone();
                                $row.find(".Name").html("<input type=’text’ class=’studentname’ id=’" + item.id + "’ value=’" + item.name + "’></input>");
                                $row.find(".delete").click(function() {

                                    $.ajax({
                                        url: "api/values/" + item.id,
                                        type: "DELETE",
                                        success: function() {
                                            $row.remove();
                                        }
                                    });

                                });

                                $row.find(".change").click(function() {
                                    var student = {
                                        id: item.id,
                                        name: $row.find(".studentname").attr("value")
                                    };

                                    $.ajax({
                                        url: "api/values/" + item.id,
                                        type: "PUT",
                                        contentType: "application/json; charset=utf-8",
                                        data: JSON.stringify(student),
                                        success: function() {

                                        }
                                    });

                                });

                                $students.append($row);

                            });
                        }
                    });
                });

               function addStudent() {                   
                   

                    var student = {
                       name: $("#frm").find("#name").attr("value"),
                    };
                   $.ajax({
                        url: "api/values",
                        type: "POST",
                        contentType: "application/json; charset=utf-8",
                        data: JSON.stringify(student),
                        success: function() {
                            alert("Added!");
                        }
                    });

                }
            </script>
        </div>
    </section>
    <section class="content-wrapper main-content clear-fix">
       
        <div id="templates" style="display:none">
           
            <table>
               
                <tr class="row-template">
                    <form>
                    <td class="Name"></td>
                  
<td><input type="button" value ="Change" class="change"/></td>
                    <td><input type="button" value ="Del" class="delete"/></td>
                    </form>
                </tr>
                 
            </table>
       
        </div>
             <form id="frm">
                    <input type="text" name="name" id="name"/>
                    <td><input type="button" value ="Add" onclick="return addStudent();"/></td>
                   
                    </form>

    </section>
</div>

Сымитируем поведение операций создания, обновления и удаления с помощью curl и Fiddler. Обратите внимание, что в curl тело сообщения задается особым образом – если у нас сложная модель, то вводится name=Sychev, например, если какое-то одиночное значение, то необходимо, чтобы тело выглядело как -d "=Sychev".

POST:

image

image

 

PUT:

image

image

DELETE:

image

 

image

 

А вот, что бывает, если возвращать клиенту в ответе сообщение при ошибке.

 

image

Все, у нас готов REST-сервис на Web API с использованием ASP.NET MVC и jQuery. Теперь рассмотрим интересную дополнительную функциональность.

Поддержка OData в Web API

ASP.NET Web API имеет встроенную поддержку для некоторых параметров запросов OData, например, сортировки, фильтрации и разбиения на подмножества (paging).

Для того, чтобы начать использовать OData в Web API, необходимо установить соответствующий пакет с помощью NuGet. Вместе с пакетом OData установится пачка зависимостей.

image

 

Заменим метод Get на следующий код.

[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
      public IQueryable<Student> Get()
      {
          return _ctx.Student.AsQueryable();
      }

Для использования метода в OData нужно пометить его как [Queryable] и указать, что он должен возвращать IQueryable. Сериализовать данные OData будет несколько иным образом, каким – мы посмотрим чуть ниже.

Теперь добавим в файл WebApiConfig в папке App_Start несколько вкусностей, которые попробуем опять же чуть ниже. Код WebApiConfig должен выглядеть как код ниже.

 

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
       
GlobalConfiguration.Configuration.Formatters.JsonFormatter.AddQueryStringMapping("$format", "json", "application/json");
        GlobalConfiguration.Configuration.Formatters.XmlFormatter.AddQueryStringMapping("$format", "xml", "application/xml");

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }

Запустим проект и перейдем по ссылке http://localhost:[port]/api/values. Будет выведен XML, возвращаемый методом по OData.

image

Обратите внимание, что данные сериализуются несколько по-иному. Это нужно учитывать при разработке.

Теперь можно воспользоваться возможностями OData. Например, перейти по такой ссылке:

http://localhost:61020/api/values?$filter=(id eq 6)

Или по такой:

http://localhost:61020/api/values?$filter=(id eq 6)&$format=json

Format – нестандартная процедура, мы ее добавили дополнительно в файл WebApiConfig.

Используя OData, можно с помощью формирования ссылок в специальном легкоусвояемом расширяемом формате взаимодействовать с моделью данных, хранимой на сервере. Конечно же, нужно быть аккуратным, несмотря на то, что Web API OData предоставляет read only доступ к данным.

 

Спасибо за внимание.

 

 

2 Comments

Leave a Comment
  1. test / May 30 2013 8:34 am

    Не получилось вызвать с помощью $.ajax({
    url: “api/values/” + item.id,
    type: “PUT”,
    contentType: “application/json; charset=utf-8”,
    data: JSON.stringify(student),
    success: function() {

    }
    });
    метод PUT

Leave a comment