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.
Рис. 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.
Рис. 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.
Рис. 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).
Рис. 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).
Рис. 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).
Рис. 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.
Рис. 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)