OData и AtomPub

Создание сервера AtomPub с применением WCF Data Services

Крис Селлз

Загрузка примера кода

Если вы еще не знакомы с Open Data Protocol (OData), то знайте, что это само изящество. OData (детально описывается на odata.org) опирается в публикации данных на Atom, основанный на HTTP, в создании, обновлении и удалении данных — на AtomPub и в определении типов данных — на Microsoft Entity Data Model (EDM).

При наличии JavaScript-клиента вы можете получить данные прямо в формате JSON вместо Atom, а в остальных случаях, в том числе из Excel, Microsoft .NET Framework, PHP, AJAX и др., предлагаются клиентские библиотеки для формирования запросов и использования ответов OData. Если вы применяете .NET Framework на серверной стороне, Microsoft также предоставляет простую в использовании библиотеку WCF Data Services для доступа к типам .NET Framework или базам данных, поддерживаемых Microsoft Entity Framework как источники OData. Это упрощает обеспечение доступа к вашим данным через Интернет по HTTP и с использованием любых стандартов.

Замечу, что вы, весьма вероятно, захотите делать с OData что-то такое, для чего нет готовой поддержки, например интегрировать OData с существующими «читателями» и «писателями» на основе Atom и AtomPub. Именно с этим мы и поэкспериментируем.

Простой блог

В качестве примера представим, что я создаю простую систему блогов (и фактически эта работа основана на переписанной мной системе управления контентом для сайта sellsbrothers.com). Будучи ярым сторонником поддержки Model-First в Visual Studio 2010, я создал проект ASP.NET MVC 2.0, добавил файл для ADO.NET EDM под именем MyBlogDB.edmx и спроектировал сущность Post, как показано на рис. 1.

image: A Post Entity Created in Visual Studio 2010

Рис. 1. Сущность Post, созданная в Visual Studio 2010

Более сложное ПО для поддержки блогов потребовало бы отслеживания большего количества данных, но поля на рис. 1 являются основными. Щелкнув правой кнопкой мыши рабочую область дизайнера, я могу выбрать команду Generate Database From Model.После этого показывается SQL-файл, который будет создан за меня (MyBlogDB.sql в данном случае), и SQL-код, который будет сгенерирован для создания моей базы данных. Щелкнув Finish, я получу SQL-файл и свяжу базу данных с сущностями, созданными в дизайнере EDM. Наиболее важные части SQL-кода показаны на рис. 2.

Рис. 2. Код SQL, полученный от «Generate Database From Model»

...
USE [MyBlogDB];
GO
...
-- Dropping existing tables
IF OBJECT_ID(N'[dbo].[Posts]', 'U') IS NOT NULL
    DROP TABLE [dbo].[Posts];
GO
...
-- Creating table 'Posts'
CREATE TABLE [dbo].[Posts] (
    [Id] int IDENTITY(1,1) NOT NULL,
    [Title] nvarchar(max)  NOT NULL,
    [PublishDate] datetime  NULL,
    [Content] nvarchar(max)  NOT NULL
);
GO
...
-- Creating primary key on [Id] in table 'Posts'
ALTER TABLE [dbo].[Posts]
ADD CONSTRAINT [PK_Posts]
    PRIMARY KEY CLUSTERED ([Id] ASC);
GO

В целом, мы просто создаем единственную таблицу из нашей единственной сущности и сопоставляем поля SQL-типам. Заметьте, что PublishDate установлен в NULL, что отличается от значения по умолчанию. Я умышленно выбрал это значение в дизайнере EDM, так как мне не нужна публикация даты (в некоторых средствах разработки значение по умолчанию вообще не предоставляется).

Чтобы выполнить этот SQL-код и создать базу данных, достаточно щелкнуть правой кнопкой мыши этот SQL в текстовом редакторе Visual Studio и выбрать команду Execute SQL. Появится запрос строки подключения и имени базы данных. Так как это новая база данных, вы должны ввести новое имя, например MyBlogDB, и щелкнуть OK для ее создания, когда появится соответствующий запрос. После создания базы данных вы можете просматривать ее в Server Explorer под соединением, только что созданным Visual Studio.

Чтобы упростить тестирование, вы можете добавить данные прямо в таблицу, щелкнув правой кнопкой мыши Posts и выбрав Show Table Data, в результате чего вы получите небольшую сетку, как показано на рис. 3.

image: The Show Table Data Grid Makes Testing Easier

Рис. 3. Grid Show Table Data упрощает тестирование

Это, конечно, не ахти какой способ редактирования, но все лучше, чем самому писать SQL-выражения от начала и до конца.

Теперь, когда у нас есть какие-то данные, можно написать немного кода для ASP.NET, чтобы выводить их, обновляя HomeController.cs (подробнее о MVC см. на сайте asp.net/mvc/):

...
namespace ODataBloggingSample.Controllers {
  [HandleError]
  public class HomeController : Controller {
    MyBlogDBContainer blogDB = new MyBlogDBContainer();

    public ActionResult Index() {
      return View(blogDB.Posts);
    }

    public ActionResult About() {
      return View();
    }
  }
}

Здесь я всего лишь создал экземпляр класса MyBlogDBContainer, который является классом верхнего уровня, производным от ObjectContext.Он создан из моего файла MyBlogDB.edmx для доступа к новой базе данных. (Если вы не знакомы с Entity Framework, обязательно прочитайте статью msdn.com/data/aa937723.) Когда вызывается метод Index класса HomeController, кто-то запрашивает основную страницу нашего нового веб-приложения, на которой мы хотели бы показывать новые публикации в блоге.Поэтому мы отправляем набор Posts из базы данных в экземпляр представления Home/Index.aspx, модифицированного следующим образом:

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
    Inherits="System.Web.Mvc.ViewPage<IEnumerable<ODataBloggingSample.Post>>" %>

<asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>
<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    <% foreach (var post in Model) { %>
      <h1><%= post.Title %></h1>
      <div><%= post.Content %></div>
      <p><i>Posted <%= post.PublishDate %></i></p>
    <% } %>
</asp:Content>

Здесь мы изменили базовый класс так, чтобы он принимал набор типа Post, генерируемый (наряду с классом MyBlogDBContainer) для моделирования нашей таблицы Posts. Кроме того, мы заменили контент основной страницы выражением foreach для отображения заголовка, содержимого и даты публикации каждого сообщения в блоге.

И это все, что нужно. Теперь, когда мы запускаем проект (Debug | Start Debugging), запускается браузер и выводятся сообщения в блоге (только одно сообщение, пока вы не поместите другие сообщения в базу данных), как показано на рис. 4.

image: The Completed Web Page

Рис. 4. Законченная веб-страница

Теперь хочу сказать следующее. Причина, по которой OData столь потрясающ, заключается в том, что, разок взмахнув мышью и пару раз тряхнув клавиатурой, я могу предоставить полный программный интерфейс к этим данным для доступа из JavaScript, .NET Framework, PHP и др. Чтобы увидеть всю эту магию в деле, щелкните правой кнопкой мыши свой проект в Solution Explorer, выберите Add | New Item, укажите WCF Data Service, присвойте имя (я использовал «odata.svc») и щелкните Add.После этого будет сгенерирован скелетный код в файле (odata.svc.cs в данном случае), который, если игнорировать пока вопросы безопасности, мы хотели бы примерно таким:

using System.Data.Services;
using System.Data.Services.Common;
using ODataBloggingSample;

namespace ODataBloggingSample {
  public class odata : DataService<MyBlogDBContainer> {
    public static void InitializeService(DataServiceConfiguration config) {
      config.SetEntitySetAccessRule("*", EntitySetRights.All);
      config.DataServiceBehavior.MaxProtocolVersion =  
        DataServiceProtocolVersion.V2;
    }
  }
}

Заметьте, что мы передаем MyBlogDBContainer (наш класс верхнего уровня для доступа к базе данных) как параметр-шаблон в класс DataService, который является основой WCF-сервиса данных на серверной стороне (см.msdn.com/data/bb931106). Класс DataService позволяет легко предоставлять доступ к нашей базе данных через определенные в протоколе OData CRUD-операции (create, read, update and delete) на основе HTTP-команд. Тип, переданный в DataService, анализируется на предмет открытых свойств, предоставляющих наборы. В нашем примере класс объектного контекста, сгенерированный Entity Framework, содержит набор Posts, который отлично подходит для этой цели:

...
namespace ODataBloggingSample {
  ...
  public partial class MyBlogDBContainer : ObjectContext {
    ...
    public ObjectSet<Post> Posts {...}
   ...
  }

  ...
  public partial class Post : EntityObject {
    ...
    public global::System.Int32 Id { get { ... } set { ... } }
    public global::System.String Title { get { ... } set { ... } }
    public Nullable<global::System.DateTime> PublishDate { 
      get { ... } set { ... } }
    public global::System.String Content { get { ... } set { ... } }
    ...
  }
}

Сгенерированный MyBlogDBContainer предоставляет ObjectSet (который является разновидностью набора) с именем Posts и содержит экземпляры типа Post. Более того, тип Post определен так, чтобы обеспечить сопоставление свойств Id, Title, PublishDate и Content нижележащим полям в созданной нами ранее таблице Posts.

Получив файл odata.svc, мы можем перейти к документу сервиса, который предоставляет свойства-наборы нашего объектного контекста с использованием имени файла конечной точки сервиса данных в URL, например localhost:54423/odata.svc:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<service xml:base="http://localhost:54423/odata.svc/" xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
    <workspace>
     <atom:title>Default</atom:title>
      <collection>
        <atom:title>Posts</atom:title>
      </collection>
    </workspace>
</service>

Этот файл целиком определяется спецификацией AtomPub (ietf.org/rfc/rfc5023.txt). Спустившись на уровень глубже, мы увидим, что наши публикации (сообщения в блоге) предоставляются как набор записей Atom по адресу localhost:54423/odata.svc/Posts (рис. 5).

Рис. 5. Публикации, представленные как набор записей Atom

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<feed xml:base="http://localhost:54423/odata.svc/"
  xmlns:d="https://schemas.microsoft.com/ado/2007/08/dataservices"
  xmlns:m=
    "https://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
  xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Posts</title>
  <id>http://localhost:54423/odata.svc/Posts</id>
  <updated>2010-03-15T00:26:40Z</updated>
  <link rel="self" title="Posts" href="Posts" />
  <entry>
    <id>http://localhost:54423/odata.svc/Posts(1)</id>
    <title type="text" />
    <updated>2010-03-15T00:26:40Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="Post" href="Posts(1)" />
    <category term="MyBlogDB.Post"
      scheme=
        "https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">1</d:Id>
        <d:Title>My first blog post</d:Title>
        <d:PublishDate m:type=
          "Edm.DateTime">2010-03-14T00:00:00</d:PublishDate>
        <d:Content>Hi! How are you?</d:Content>
      </m:properties>
    </content>
  </entry>
</feed>

Этот файл почти полностью соответствует обычному формату Atom (ietf.org/rfc/rfc4287.txt) за исключением URI в формате от Microsoft, который используется для создания уровня функциональности OData в Atom. В частности, обратите внимание на элемент properties в элементе content. Вы заметите, что эти свойства точно соответствуют тем, которые ранее были определены в сущности Post и таблице Posts. Эти данные содержатся в конверте (envelope), определенном Atom, и предоставляются через CRUD-комментарии, которые в свою очередь определяются AtomPub и
позволяют создавать, считывать, обновлять и удалять данные через HTTP-методы POST, GET, PUT и DELETE. Проблема в том, что это уже далеко не обычный формат Atom. Например, если вы перейдете к odata.svc/Posts в средстве чтения Atom, например в Internet Explorer 8, то заголовок и содержимое не будут показываться должным образом (рис. 6).

image: Viewing Blog Posts in Atom Reader Shows That Title and Content Are Missing

Рис. 6. Просмотр записей блога в средстве чтения Atom показывает, что заголовок и содержимое отсутствуют

Как видите, данные на месте (заметьте, что дата правильная и категория показывается), но заголовок и контент отсутствуют. Это связано с тем, что Internet Explorer ищет заголовок и контент в элементах title и content в каждой записи (что достаточно логично), но эти элементы не содержат того, что ожидалось. Элемент title пуст, а элемент content имеет формат, не распознаваемый Internet Explorer. Формат, который понравился бы Internet Explorer, должен выглядеть так:

<feed ...>
  <title type="text">Posts</title>
  <id>http://localhost:54423/atompub.svc/Posts</id>
  <updated>2010-03-15T00:42:32Z</updated>
  <link rel="self" title="Posts" href="Posts" />
  <entry>
    <id>http://localhost:54423/atompub.svc/Posts(1)</id>
    <title type="text">My first blog post</title>
    <updated>2010-03-15T00:42:32Z</updated>
    ...
    <content type="html">Hi! How are you?</content>
    <published>2010-03-14T00:00:00-08:00</published>
  </entry>
</feed>

Заметьте, что элемент title содержит то, что было похоронено в свойстве Title из OData-элемента properties в элементе content, элемент content перезаписан свойством Content, а элемент published был добавлен из значения свойства PublishDate. Когда эти данные просматриваются в Internet Explorer, мы получаем гораздо более близкое к задуманному содержимое окна (рис. 7).

image: Tweaking the XML Format Results in Correct Display of the Title and Content

Рис. 7. Настройка формата XML приводит к верному отображению заголовка и содержимого

Должен упомянуть, что нас волнует только поддержка блогов. Internet Explorer ожидает не списков клиентов или счетов, а заголовков, дат публикации и HTML-контента. Иногда имеет смысл создавать такое же сопоставление для списков клиентов и счетов, в каковом случае Microsoft предлагает в WCF Data Services механизм под названием «Friendly Feeds» («дружественные каналы») (см. blogs.msdn.com/astoriateam/archive/ 2008/09/28/making-feeds-friendly.aspx). Однако он не годится на все случаи жизни (в частности, он не переопределяет сопоставление Atom-элемента content), поскольку цель группы WCF Data Services состояла в том, чтобы обеспечить даже работу «дружественных» каналов с различными клиентскими библиотеками. То есть смысл в том, чтобы сделать дружественными каналы OData, а не отказаться от OData в пользу Atom/AtomPub.

В данном случае, тем не менее, мы отказываемся от OData и просто используем WCF Data Services как конечную точку AtomPub, которая требует сопоставления между Atom и OData (рис. 8).

image: Mapping Between Atom and OData

Рис. 8. Сопоставление между Atom и OData

Фокус в том, как создать такое сопоставление? Мы получили данные, но нам нужно переопределить их сопоставление и подключить к свойствам Atom, чтобы «читатели» Atom (и «писатели») знали, где находятся данные. Причина, по которой мы делаем это, заключается в том, что тогда WCF Data Services по-прежнему сможет выполнять сопоставление с нашими типами .NET Framework или — через Entity Framework — с нашими базами данных. Нам нужно лишь создавать по месту двухстороннее сопоставление Atom/AtomPub с OData.

В примере, который можно скачать для этой статьи, содержится код, встраиваемый в конвейер WCF и позволяющий выполнять как раз такое преобразование сообщений. Вы можете детально изучить его (см. файл ODataBlogging.cs), но я собираюсь рассказать лишь о том, как им пользоваться.

Сначала создайте новую конечную точку WCF Data Services (так, как вы уже делали это), но с другим именем (я указал atompub.svc). Подключите класс объектного контекста верхнего уровня и откройте доступ к любым нужным вам наборам сущностей, но на этот раз пометьте свой класс сервиса атрибутом ODataBloggingServiceBehavior:

...
using ODataBlogging;

namespace ODataBloggingSample {
  [ODataBloggingServiceBehavior(typeof(MyBlogDBContainer))]
  [EntityAtomMapping("Posts", "PublishDate", "published")]
  public class atompub : DataService<MyBlogDBContainer> {
    public static void InitializeService(DataServiceConfiguration config) {
      config.SetEntitySetAccessRule("*", EntitySetRights.All);
      config.DataServiceBehavior.MaxProtocolVersion = 
        DataServiceProtocolVersion.V2;
    }
  }
}

Это обеспечивает сопоставление от Atom/AtomPub (например, элементов title, content и published) с соответствующим форматом OData через элемент properties, вложенный в элемент content. По умолчанию, если имена сущностей совпадают (регистр букв игнорируется), сопоставление (и приведение типов) начинает работать. Например, когда предоставляемая сущность содержит свойство Title (как и наша сущность Post), она сопоставляется с Atom-элементом title.

С другой стороны, если нет автоматического сопоставления, вы можете переопределить поведение, предоставив явное сопоставление на основе имени сущности, как это сделали мы при сопоставлении свойства PublishDate для объектов в наборе Posts с Atom-свойством published. Эти двух атрибутов достаточно, чтобы преобразовать наш OData-канал в Atom-канал, что дает нам полнофункциональное представление данных, как показано на рис. 7.

Это сопоставление не одностороннее — оно поддерживает все HTTP-методы, поэтому вы можете использовать протокол AtomPub для создания, обновления и удаления элементов в наборе Posts, а также читать их. То есть вы можете сконфигурировать какое-нибудь средство вроде Windows Live Writer (WLW), которое поддерживает AtomPub как API блогов, и применять его в качестве полнофункционального редактора своих публикаций (сообщений). Например, при наличии конечной точки atompub.svc вы могли бы выбрать в WLW команду Blogs | Add blog account и заполнить следующие параметры в диалогах:

  • Какую службу блогов вы используете: Другая служба блогов
  • Веб-адрес вашего блога: http://<<server>>:<<port>>/atompub.svc
  • Имя пользователя: <<имя_пользователя>> (требуется и должно быть реализовано в вашей конечной точке AtomPub с применением стандартных HTTP-методов)
  • Пароль: <<пароль>>
  • Тип используемого блога: Atom Publishing Protocol
  • URL-адрес служебного документа: http://<<server>>:<<port>>/atompub.svc
  • Псевдоним блога: <<какой_хотите>>

Щелкните кнопку Finish и вы получите полнофункциональный текстовый редактор для управления публикациями в своем блоге, как показано на рис. 9.

image: Atom/OData Mapping Facilitates Building a Rich Text Editor to Manage Your Blog Posts

Рис. 9. Сопоставление Atom/OData упрощает создание полнофункционального текстового редактора для управления записями блогов

Здесь мы взяли механизм Data Services, который поддерживает всю CRUD-функциональность, упаковывая свойства в Atom-элемент content, и приложили немного усилий для сопоставления, чтобы заодно обеспечить поддержку чистых Atom и AtomPub.

Небольшая библиотека-пример, которую я использовал для выполнения этой работы — и создал ее на пару с Фани Раджем (Phani Raj), инженером по программному обеспечению из группы Microsoft WCF Data Services, — делает самый-самый минимум и совершенно недостаточна для создания реального блога. Вот список того, что по минимуму нужно для реальной поддержки только Atom и AtomPub:

  • сопоставление с элементами, вложенными в Atom-элемент author, например name, uri и e-mail;
  • обработка изображений (хотя WLW поддерживает FTP, и этого может оказаться достаточно);
  • предоставление средств, которые позволят WLW распознавать всю эту функциональность.

Если вы заинтересованы в продолжении этого эксперимента, тогда читайте серию статей Джо Чена (Joe Cheng), члена группы WLW, о поддержке AtomPub в WLW в блоге: jcheng.wordpress.com/2007/10/15/how-wlw-speaks-atompub-introduction.

Наслаждайтесь!

Крис Селлз - руководитель программы Майкрософт в подразделении бизнес-платформ. Он является автором нескольких книг, а также соавтором «Programming WPF» (O’Reilly Media, 2007), «Windows Forms 2.0 Programming» (Addison-Wesley Professional, 2006) и «ATL Internals» (Addison-Wesley Professional, 1999). В свободное время он проводит различные конференции, а также участвует в создании списков обсуждения отдела внутренней продукции Майкрософт. Более подробная информация о Селлзе и его различных проектах доступна по адресу sellsbrothers.com.

Выражаю благодарность следующим экспертам за рецензирование статьи:. Пабло Кастро (Pablo Castro)