Доступ к данным

Новый вариант создания OData-каналов: Web API

Джули Лерман

Исходный код можно скачать по ссылке.

Джули ЛерманРазработчики, использующие Microsoft .NET, могли создавать OData-каналы даже до появления спецификации OData. С помощью WCF Data Services можно было бы предоставлять Entity Data Model (EDM) через Web, применяя Representational State Transfer (REST). Иначе говоря, вы могли бы использовать эти сервисы через HTTP-вызовы GET, PUT, DELETE и т. д. Поскольку инфраструктура для создания этих сервисов постоянно развивалась (и несколько раз переименовывалась), эволюционировал и вывод, определение которого в конечном счете было помещено в спецификацию OData (odata.org). Теперь существует множество клиентских API для использования OData из .NET, PHP, JavaScript и многих других клиентов. Но до недавнего времени единственным простым способом создания сервиса был тот, который основан на применении WCF Data Services.

WCF Data Services — .NET-технология, которая просто позволяет обертывать вашу EDM (.edmx или модель, определенную через Code First), а затем предоставлять эту модель для запросов и обновления по HTTP. Поскольку вызовы осуществляются в виде URI (например, http://mysite.com/mydataservice/Clients(34)), запросы можно выдавать даже из веб-браузера или такой утилиты, как Fiddler. Чтобы создать WCF-сервис данных, в Visual Studio можно выбрать шаблон для построения сервиса данных с применением набора API.

Теперь есть и другой вариант создания OData-каналов: применение ASP.NET Web API. В этой статье я дам высокоуровневый обзор некоторых различий между этими двумя подходами и расскажу о причинах, по которым вы могли бы предпочесть один из этих подходов. Кроме того, мы рассмотрим способы создания OData API, отличные от создания Web API.

Высокоуровневое сравнение API и сервиса данных

WCF-сервис данных — это System.Data.Services.DataService, который обертывает уже определенный вами ObjectContext или DbContext. Когда вы объявляете класс сервиса, он является обобщенным DataService вашего контекста (т. е. DataService<MyDbContext>). Так как изначально он полностью блокирован, вы задаете разрешения доступа в конструкторе DbSets из своего контекста, который должен предоставляться сервисом. Это все, что вам нужно сделать. Об остальном заботится нижележащий DataService API: напрямую взаимодействует с вашим контекстом, запрашивает и обновляет базу данных в ответ на HTTP-запросы клиентского приложения к сервису. Кроме того, можно в какой-то мере настраивать сервис, переопределяя часть его логики запросов и обновления. Но по большей части смысл в том, чтобы делегировать DataService основную работу по взаимодействию с контекстом.

С другой стороны, Web API позволяет определять взаимодействие с контекстом в ответ на HTTP-запросы (PUT, GET и подобные им). Этот API предоставляет методы, и логику методов определяете вы. Вам не обязательно взаимодействовать с Entity Framework или даже с базой данных. У вас могут быть объекты в памяти, которые клиент запрашивает или передает. Но точка доступа не создается волшебным образом, как это происходит в случае использования WCF-сервиса данных; вместо этого вы управляете тем, что происходит в ответ на вызовы. Это решающий фактор при выборе между сервисом и API для предоставления ваших OData-данных. Если вы хотите в основном предоставлять простые CRUD-операции (Create, Read, Update, Delete) без особой адаптации, лучшим выбором будет сервис данных. А если вам нужно серьезно модифицировать поведение, гораздо лучше использовать Web API.

Мне нравится, как на недавнем собрании высказался по этому поводу Мэтт Милнер (Matt Milner), MVP в области интеграции технологий Microsoft: «WCF Data Services предназначена для того случая, когда вы начинаете с данных и модели и просто хотите предоставить к ним доступ. Web API удобнее, когда вы начинаете с API и хотите определить то, что он должен предоставлять».

Закладываем фундамент с помощью стандартного Web API

Для тех, кто имеет ограниченный опыт работы с Web API, я сочла полезным до рассмотрения новой поддержки OData дать обзор основ Web API, а затем показать, как они соотносятся с созданием Web API, который предоставляет данные через OData. Я сделаю это прямо здесь: сначала создам простой Web API, использующий Entity Framework в качестве уровня доступа к данным, а потом преобразую его так, чтобы он предоставлял результаты в формате OData.

Одно из применений Web API — альтернатива стандартному контроллеру в MVC-приложении (Model-View-Controller), и вы можете создать его как часть проекта ASP.NET MVC 4. Если клиентский интерфейс вам не нужен, можно начать с пустого веб-приложения ASP.NET и добавить контроллеры Web API. Однако ради новичков я начну с шаблона ASP.NET MVC 4, так как он обеспечивает формирование шаблонов (scaffolding), которые генерируют кое-какой стартовый код. Как только вы поймете, как соотносятся все части, правильнее будет начинать с пустого проекта.

Итак, я создам новое приложение ASP.NET MVC 4 и после приглашения выберу шаблон Empty (а не шаблон Web API, который предназначен для более надежных приложений, использующих представления, и является перебором для моих целей). В результате я получаю проект, структурированный для MVC-приложения с пустыми папками Models, Views и Controllers. На рис. 1 сравниваются результаты применения шаблона Empty с таковыми для шаблона Web API. Как видите, шаблон Empty дает гораздо более простую структуру, и мне нужно лишь удалить несколько папок.

Проекты ASP.NET MVC 4 на основе шаблонов Empty и Web API
Рис. 1. Проекты ASP.NET MVC 4 на основе шаблонов Empty и Web API

Мне также не требуется папка Models, потому что я использую существующий набор классов домена и DbContext в раздельных проектах, которые предоставляют модель. Затем с помощью инструментария Visual Studio я создаю свой первый контроллер — он будет контроллером Web API для взаимодействия с моим DbContext и классами домена, на которые я сослалась из MVC-проекта. Моя модель содержит классы для Airline, Passengers, Flights и некоторых дополнительных типов для данных, связанных с авиарейсами.

Поскольку я задействовала шаблон Empty, мне понадобится добавить ссылки, чтобы вызывать DbContext: одна — на System.Data.Entity.dll и вторая — на EntityFramework.dll. Вы можете добавить обе эти ссылки, установив NuGet-пакет EntityFramework.

Новый контроллер Web API можно создать так же, как стандартный MVC-контроллер: щелкните правой кнопкой мыши папку Controllers в решении и выберите Add, а потом Controller. Как видно на рис. 2, теперь у вас есть шаблон для создания API-контроллера с EF-операциями чтения и записи. Также имеется контроллер Empty API. Давайте начнем с EF-операций чтения/записи в качестве точки сравнения с контроллером, необходимым нам для OData, который тоже будет использовать Entity Framework.

Шаблон для создания API-контроллера с заранее указанными операциями
Рис. 2. Шаблон для создания API-контроллера с заранее указанными операциями

Если вы создавали когда-нибудь MVC-контроллеры, то заметите, что полученный класс аналогичен, но вместо набора методов для операций, относящихся к представлению, таких как Index, Add и Edit, этот контроллер имеет набор HTTP-операций.

Например, в нем есть два Get-метода, как показано на рис. 3. Сигнатура первого из них, GetAirlines, не принимает никаких параметров и использует экземпляр AirlineContext (механизм формирования шаблонов присвоил ему имя db) для возврата набора экземпляров Airline в Enumerable. Другой метод, GetAirline, принимает целочисленный параметр и использует его для поиска и возврата конкретного экземпляра Airline.

Рис. 3. Некоторые из методов контроллера Web API, созданные механизмом формирования шаблонов

public class AirlineController : ApiController
  {
    private AirlineContext db = new AirlineContext2();
    // GET api/Airline
    public IEnumerable<Airline> GetAirlines()
    {
      return db.Airlines.AsEnumerable();
    }
    // GET api/Airline/5
    public Airline GetAirline(int id)
    {
      Airline airline = db.Airlines.Find(id);
      if (airline == null)
      {
        throw new HttpResponseException
          (Request.CreateResponse(HttpStatusCode.NotFound));
      }
      return airline;
    }

Шаблон добавляет комментарии, демонстрирующие, как вы могли бы использовать эти методы.

Подготовив конфигурации для своего контроллера Web API, я могу проверить его прямо в браузере, используя синтаксис примера для порта, назначенного моему приложению: http://localhost:1702/api/Airline. Это HTTP-вызов GET по умолчанию, и поэтому он направляется приложением для выполнения в метод GetAirlines. Web API использует процесс согласования контента (content negotiation), чтобы определить, как форматировать набор результатов. В качестве браузера по умолчанию я использую Google Chrome, что приводит к форматированию результатов в виде XML. Формат результатов определяется запросом от клиента. Например, Internet Explorer не отправляет никакой специфической информации относительно принимаемого формата, поэтому Web API по умолчанию возвращает JSON. Мои XML-результаты приведены на рис. 4.

Рис. 4. Ответ WebAPI Airline на GET, показываемый в виде XML в моем браузере

<ArrayOfAirline xmlns:i=http://www.w3.org/2001/XMLSchema-instance
  xmlns="http://schemas.datacontract.org/2004/07/DomainClasses">
    <Airline>
      <Id>1</Id>
      <Legs/>
      <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>
      <Name>Vermont Balloon Transporters</Name>
    </Airline>
    <Airline>
      <Id>2</Id>
      <Legs/>
      <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>
      <Name>Olympic Airways</Name>
    </Airline>
    <Airline>
      <Id>3</Id>
      <Legs/>
      <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>
      <Name>Salt Lake Flyer</Name>
    </Airline>
</ArrayOfAirline>

Если бы я добавила целочисленный параметр в запрос (в соответствии с примером в комментарии — http://localhost:1702/api/Airline/3), то получила бы только одну авиалинию (airline), чей ключ (Id) равен 3:

<Airline xmlns:i=http://www.w3.org/2001/XMLSchema-instance
  xmlns="http://schemas.datacontract.org/2004/07/DomainClasses">
    <Id>3</Id>
    <Legs/>
    <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>
    <Name>Salt Lake Flyer</Name>
</Airline>

Если бы я использовала Internet Explorer или утилиту вроде Fiddler, где можно было бы явно управлять запросом к API, чтобы гарантированно получить JSON, то результат запроса Airline с Id, равным 3, был бы возвращен как JSON:

{"Id":3,
  "Name":"Salt Lake Flyer",
  "Legs":[],
  "ModifiedDate":"2013-03-17T00:00:00"
}

Эти ответы содержат простые представления типов авиалиний с элементами для каждого свойства: Id, Legs, ModifiedDate и Name.

Контроллер также содержит метод PutAirline, вызываемый Web API в ответ на HTTP-запрос PUT. PutAirline включает код, используемый AirlineContext при обновлении Airline. Кроме того, есть метод PostAirline для вставок и метод DeleteAirline для удаления. Это нельзя продемонстрировать в URL браузера, но вы можете найти уйму учебных материалов по Web API в MSDN, Pluralsight и на других сайтах, поэтому я перейду к переделке этого контроллера, так чтобы он выводил свои результаты согласно спецификации OData.

Превращение Web API в провайдер OData

Теперь, когда у вас есть базовое представление о том, как с помощью Web API можно предоставлять данные с применением Entity Framework, рассмотрим особый случай использования Web API для создания провайдера OData на основе модели данных. Вы можете указать контроллеру Web API возвращать данные, отформатированные как OData, превратив его в контроллер OData (для этого применяется класс, доступный в пакете ASP.NET and Web Tools 2012.2), а затем переопределив его методы, специфичные для OData. Благодаря контроллеру этого нового типа вам даже не понадобятся методы, созданные шаблоном. По сути, более эффективный путь к созданию контроллера OData — выбор шаблона Empty Web API, а не того, который создает код для CRUD-операций.

Для такого перехода нужно выполнить четыре операции.

  1. Создать контроллер типа ODataController и реализовать его HTTP-методы. С этой целью я воспользуюсь сокращенным способом.
  2. Определить доступные EntitySet в файле WebAPIConfig проекта.
  3. Сконфигурировать маршрутизацию (routing) в WebAPIConfig.
  4. Присвоить классу контроллера имя во множественном числе, как того требуют соглашения OData.

Создание ODataController Вместо того чтобы напрямую наследовать от ODataController, я использую EntitySetController, производный от ODataController и обеспечивающий более высокоуровневую поддержку за счет ряда виртуальных CRUD-методов. С помощью NuGet я устанавливаю пакет Microsoft ASP.NET Web API OData, чтобы получить необходимые сборки, содержащие оба класса контроллеров.

Вот начало моего класса, теперь наследующего от EntitySetController и указывающего, что данный контроллер предназначен для типа Airline:

public class AirlinesController :  EntitySetController<Airline,int>
{
  private AirlineContext db = new AirlineContext();
  public override IQueryable<Airline> Get()
  {
    return db.Airlines;
  }

Я реализовала переопределение для метода Get, который будет возвращать db.Airlines. Заметьте: я не вызываю ToList или AsEnumerable в Airlines DbSet. Методу Get нужно возвращать IQueryable типа Airline, и именно это делает db.Airlines. Тем самым потребитель OData может определять запросы через этот набор, которые потом выполняются в базе данных, а это предотвращает запись всех Airline в память с последующей выдачей запроса к ним.

Вы можете переопределять следующие HTTP-методы (и добавлять в них логику): GET, POST (для вставок), PUT (для обновлений), PATCH (для слияния обновлений) и DELETE. Но для обновлений вы должны будете на самом деле использовать виртуальный метод CreateEntity, чтобы переопределить логику, вызываемую для POST, UpdateEntity — для логики, вызываемой с PUT, и PatchEntity — для логики, необходимой при HTTP-вызове PATCH. Дополнительные виртуальные методы, которые могут быть частью этого провайдера OData, таковы: CreateLink, DeleteLink и GetEntityByKey.

В WCF Data Services вы контролируете, какие CRUD-операции разрешаются для каждого EntitySet, конфигурируя SetEntitySetAccessRule. Но в случае Web API вы просто добавляете методы, которые хотите поддерживать, и не включаете методы, к которым потребители не должны получать доступа.

Задание наборов EntitySet для API Web API нужно знать, какие EntitySet должны быть доступны потребителям. Поначалу это сбило меня с толку. Я считала, что он распознает это, читая AirlineContext. Но хорошенько подумав, я осознала, что здесь все происходит по аналогии с применением SetEntitySetAccessRule в WCF Data Services. В WCF Data Services вы определяете, какие CRUD-операции разрешены, в тот момент, когда предоставляете доступ к конкретному набору. Но в случае Web API вы начинаете с модификации метода WebApiConfig.Register, чтобы сообщить, какие наборы будут частью API, а затем используете методы в контроллере, чтобы обеспечить доступ к конкретным CRUD-операциям. Вы указываете наборы с помощью ODataModelBuilder, похожего на DbContext.ModelBuilder, который вы, возможно, применяли с Code First. Вот код в методе Register из файла WebApiConfig, который позволяет моему OData-каналу предоставлять доступ к Airlines и Legs:

ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
                  modelBuilder.EntitySet<Airline>("Airlines");
                  modelBuilder.EntitySet<FlightLeg>("Legs");

Определение маршрута для поиска OData Далее методу Register нужен маршрут (route), который указывает на эту модель; когда вы вызовете Web API, он предоставит доступ к определенным вами EntitySet:

Microsoft.Data.Edm.IEdmModel model = modelBuilder.GetEdmModel();
config.Routes.MapODataRoute("ODataRoute", "odata", model);

Вы увидите, что во многих демонстрациях «odata» используется в качестве параметра RoutePrefix, который определяет префикс URL для ваших API-методов. Хотя это хороший стандарт, вы можете называть его как угодно, например так, как это сделала я:

config.Routes.MapODataRoute("ODataRoute", "oairlinedata", model);

Переименование класса контроллера Шаблон приложения генерирует код, в котором используется соглашение об именовании контроллеров в единственном числе наподобие AirlineController и LegController. Однако в OData основной акцент делается на наборы EntitySet, которые обычно именуются во множественном числе по имени сущности. А поскольку мои EntitySet действительно множественные, мне нужно изменить имя своего класса контроллера на AirlinesController для соответствия Airlines EntitySet.

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

Теперь я могу работать с API, используя привычный синтаксис запросов OData. Я начну с запроса списка того, что доступно по http://localhost:1702/oairlinedata/. Результаты показаны на рис. 5.

Рис. 5. Запрос списка доступных данных

http://localhost:1702/oairlinedata/
<service xmlns="http://www.w3.org/2007/app" xmlns:atom=
  "http://www.w3.org/2005/Atom"
  xml:base="http://localhost:1702/oairlinedata /">
    <workspace>
      <atom:title type="text">Default</atom:title>
      <collection href="Airlines">
        <atom:title type="text">Airlines</atom:title>
      </collection>
      <collection href="Legs">
        <atom:title type="text">Legs</atom:title>
      </collection>
    </workspace>
</service>

Результаты сообщают мне, что сервис предоставляет доступ к Airlines и Legs. После этого я запрошу список Airlines как OData с помощью http://localhost:1702/oairlinedata/Airlines. OData может возвращаться в виде XML или JSON. По умолчанию Web API возвращает результаты в формате JSON:

{
  "odata.metadata":
    "http://localhost:1702/oairlinedata/$metadata#Airlines","value":[
    {
      "Id":1,"Name":"Vermont Balloons","ModifiedDate":"2013-02-26T00:00:00"
    },{
      "Id":2,"Name":"Olympic Airways","ModifiedDate":"2013-02-26T00:00:00"
    },{
      "Id":3,"Name":"Salt Lake Flyer","ModifiedDate":"2013-02-26T00:00:00"
    }
  ]
}

Одна из многих особенностей OData URI — поддержка запросов. По умолчанию Web API не поддерживает запросы, так как они создают дополнительную нагрузку на сервер. Поэтому вы не сможете использовать эти средства запросов со своим контроллером Web API, пока не добавите аннотацию Queryable к соответствующим методам. Например, здесь я добавила Queryable к методу Get:

[Queryable]
public override IQueryable<Airline> Get()
{
  return db.Airlines;
}

Теперь можно использовать методы $filter, $inlinecount, $orderby, $sort и $top. Вот запрос, использующий OData-метод $filter:

http://localhost:1702/oairlinedata/Airlines?$filter=startswith(Name,'Vermont')

ODataController позволяет ограничивать запросы, чтобы потребители не вызывали проблем с производительностью на вашем сервере. Скажем, можно ограничить число записей, возвращаемых в одном ответе. Детали см. в статье «OData Security Guidance» по ссылке bit.ly/X0hyv3.

Заключение

Я рассмотрела лишь часть средств запросов, которые можно предоставлять через поддержку Web API OData. Вы также можете использовать виртуальные методы EntitySetController, чтобы разрешить обновление базы данных. Интересным дополнением к PUT, POST и DELETE является PATCH, который — в противоположность передаче полной сущности через POST — позволяет отправлять явный и эффективный запрос на обновление, когда изменено только небольшое количество полей. Но логика внутри вашего метода PATCH должна обрабатывать соответствующее обновление, которое в случае применения Entity Framework скорее всего подразумевает извлечение из базы данных текущего объекта и его обновление новыми значениями. Как вы реализуете эту логику, зависит от того, в какой точке рабочего процесса вы готовы к расплате за издержки передачи данных по сети. Кроме того, важно понимать, что в этом выпуске (с пакетом ASP.NET and Web Tools 2012.2) поддерживается лишь подмножество средств OData. То есть не все API-вызовы, которые вы могли бы адресовать OData-каналу, будут работать с провайдером OData, созданным с помощью Web API. В примечаниях к выпуску для пакета ASP.NET and Web Tools 2012.2 перечислены все поддерживаемые средства.

Очень многое осталось за рамками моей статьи, ограниченной местом, которое отводится данной рубрике. Я рекомендую почитать великолепную серию статей Майка Уоссона (Mike Wasson) по OData в официальной документации на Web API по ссылке bit.ly/14cfHIm. Вы узнаете о создании всех CRUD-методов, использовании PATCH и даже о применении аннотаций для ограничения типов фильтрации, допустимых в ваших OData API, а также о работе с отношениями. Учтите, что многие из прочих средств Web API относятся к OData API, например как использовать авторизацию для ограничения тех, кто имеет права на те или иные операции. Кроме того, в блоге .NET Web Development and Tools (blogs.msdn.com/webdev) вы найдете ряд публикаций о поддержке OData в Web API.


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media), а также многочисленных онлайновых учебных курсов на Pluralsight.com. Вы также можете читать ее заметки в twitter.com/julielerman.

Выражаю благодарность за рецензирование статьи экспертам из Microsoft Джону Гэллоуэю (Jon Galloway) и Майку Уоссону (Mike Wasson).