August 2010

Volume 25 Number 08

OData と AtomPub - WCF Data Services を使用した AtomPub サーバーの構築

Chris Sells | August 2010

コード サンプルのダウンロード

あまりご存知ないかもしれませんが、Open Data Protocol (OData) は見事なものです。OData (詳細については、odata.org (英語) を参照) は、データを公開するための Atom、データを作成、更新、および削除するための AtomPub、データの種類を定義するための Microsoft Entity Data Model (EDM) を基盤として、HTTP ベースで構築されています。

クライアント側で JavaScript を使用している場合は、Atom 形式ではなく、JSON 形式で直接データを返すことができます。また、Excel、Microsoft .NET Framework、PHP、AJAX などを使用している場合には、OData の要求形式を作成したり、OData の応答を利用したりするためのクライアント ライブラリがあります。サーバー側で .NET Framework を使用しているのであれば、マイクロソフトが、WCF Data Services という名前の使いやすいライブラリを提供しています。このライブラリでは、Microsoft Entity Framework によって OData のソースとしてサポートされる、.NET Framework 型やデータベースを公開します。このライブラリのおかげで、HTTP ベースかつ標準に基づいた方法を使用して、インターネット経由で簡単にデータを公開できるようになります。

ただし、OData と既存の Atom ベースおよび AtomPub ベースのリーダーやライターとの統合など、OData を使用して行う処理によっては、まったく手を加えないでそのまま操作できるというわけではありません。そこで、今回の記事では、このような操作について紹介します。

簡単なブログ

たとえば、簡単なブログ システムを構築しているとします (実は、この作業は、sellsbrothers.com (英語) のコンテンツ管理システムの書き直し作業が基になっています)。私は、Visual Studio 2010 のモデル優先のサポートがお気に入りなので、ASP.NET MVC 2.0 プロジェクトを作成し、MyBlogDB.edmx という名前の ADO.NET EDM ファイルを追加して、Post エンティティを配置しました (図 1 参照)。

Visual Studio 2010 で作成した Post エンティティ

図 1 Visual Studio 2010 で作成した Post エンティティ

複雑なブログ ソフトウェアでは、もっと多くのデータを追跡することになりますが、図 1 に示したフィールドが基本的なものです。デザイナー画面を右クリックして、[モデルからデータベースを生成] をクリックすると、作成される SQL ファイル (この場合は MyBlogDB.sql) と、データベースを作成するために生成される SQL が表示されます。[完了] をクリックすると、SQL ファイルが作成され、データベースは EDM デザイナーで作成されたエンティティにバインドされます。SQL コードの重要な部分を 図 2 に示します。

図 2 [モデルからデータベースを生成] をクリックすると生成される SQL コード

...
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 コードを実行してデータベースを作成するには、Visual Studio テキスト エディターで SQL コードを右クリックして、[SQL の実行] を選択します。接続情報とデータベース名を確認するメッセージが表示されます。これは新しいデータベースなので、MyBlogDB など、新しい名前を入力します。メッセージが表示されたら、[OK] をクリックして、データベースを作成します。データベースを作成したら、Visual Studio が作成したばかりの接続の下にそのデータベースが表示されるのをサーバー エクスプローラーで確認できます。

テストを簡単に行うには、[Posts] を右クリックし、[テーブル データの表示] をクリックして、データを直接テーブルに追加することができます。ここでは、データが、図 3 に示すような簡単なグリッド形式で表示されます。

[テーブル データの表示] をクリックしてグリッド形式で表示し、テストを簡単に実行できるようにする

図 3 [テーブル データの表示] をクリックしてグリッド形式で表示し、テストを簡単に実行できるようにする

これは世界で最もすばらしい編集方法とは言えませんが、編集ソリューション全体が完成するまでは、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 クラスで呼び出されると、新しいブログの投稿を表示するために使用する、新しい Web アプリケーションのホーム ページが要求されるため、データベースの 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>

ここでは基本クラスを変更して、Posts テーブルをモデル化するために (MyBlogDBContainer クラスと一緒に) 生成される、Post 型のコレクションを受け取ります。また、foreach ステートメントを使用して、ホーム ページのコンテンツを置き換えて、それぞれの投稿のタイトル、内容、および公開日を表示するようにします。

必要な処理は以上です。これで、プロジェクトを実行する ([デバッグ] メニューの [デバッグ開始] をクリックする) と、ブラウザーが起動して、図 4 のようにブログの投稿が表示されます (データベースに 2 つ以上保存していない限り、投稿は 1 つだけ表示されます)。

完成した Web ページ

図 4 完成した Web ページ

以上で、必要な操作についてすべて説明したので、次のように言うことができます。OData がとても優れているのは、マウスを 1 回クリックして、キーボードを 2 回叩いただけで、JavaScript、.NET Framework、PHP などからアクセスできる、このデータの完全なプログラム インターフェイスを公開できるからです。この魔法を実現するには、ソリューション エクスプローラーでプロジェクトを右クリックし、[追加] をポイントして [新しい項目] をクリックします。[WCF Data Service] を選択し、名前を入力して (ここでは「odata.svc」と入力しました)、[追加] をクリックします。これで、ファイル (この場合は 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 クラスにスローしていることに注目してください。DataService クラスは、サーバー側の WCF Data Services の中核となるものです (詳細については、msdn.com/data/bb931106 を参照してください)。DataService クラスを使用すると、OData プロトコルで定義された、HTTP 動詞ベースの作成、読み取り、更新、削除 (CRUD) の操作を通じて、データベースを簡単に公開できます。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 は、Posts という名前で、Post 型のインスタンスを含む、ObjectSet (これはコレクションのようなものです) を公開することに注目してください。さらに、Post 型は、Id、Title、PublishDate、および Content の各プロパティ間のマッピングを、先ほど作成した Posts テーブルの基になる列に提供するように定義されています。

odata.svc を適切に設定したら、たとえば、localhost:54423/odata.svc のように、URL のデータ サービス エンドポイント ファイルの名前を使用して、オブジェクト コンテキスト コレクションのプロパティを公開するサービス ドキュメントに移動できます。

<?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、英語) で定義されています。さらに詳しく説明すると、ブログの投稿が、localhost:54423/odata.svc/Posts で、一連の Atom エントリとして公開されているのを確認できます (図 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>

このファイルは、OData 機能を Atom に階層化するために使用する、Microsoft ベースの URI を除けば、まったくありきたりな Atom (ietf.org/rfc/rfc4287.txt、英語) です。特に、"content" 要素内にある "properties" 要素に注目してください。これらのプロパティが、先ほど Post エンティティで定義したものと同じで、Posts テーブルに対応していることはおわかりでしょう。このデータは Atom によって定義されたエンベロープに含まれており、CRUD コメントを通じて公開されます。CRUD コメントは AtomPub によって定義され、作成、読み取り、更新、削除を実行できます (このとき、それぞれ対応する HTTP メソッドの POST、GET、PUT、および DELETE を使用します)。問題は、これがありきたりの Atom と言いきってしまえないことです。たとえば、Internet Explorer 8 のような Atom リーダーで odata.svc/Posts に移動すると、タイトルと内容が正しく表示されません (図 6 参照)。

Atom リーダーでブログの投稿を閲覧するとタイトルと内容が表示されない

図 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" 要素には、"content" 要素内の OData の "properties" 要素にある、Title プロパティに格納するために使用された値が含まれており、"content" 要素は Content プロパティで上書きされ、"published" 要素は PublishDate プロパティの値から追加されていることに注目してください。このデータを Internet Explorer で表示すると、期待していた内容が表示されます (図 7 参照)。

XML 形式を調整してタイトルと内容を適切に表示する

図 7 XML 形式を調整してタイトルと内容を適切に表示する

ここでは、ブログ ツールのサポートのみに注目しているにすぎません。Internet Explorer では、顧客一覧や請求書を表示することを想定しているのではなく、タイトル、公開日、および HTML コンテンツを表示することを想定しています。場合によっては、顧客一覧と請求書について、これまでのようにマッピングを作成することになりますが、マイクロソフトはこのような場合に備えて、WCF Data Services で "わかりやすいフィード" と呼ばれる機能を提供しています (詳細については、blogs.msdn.com/astoriateam/archive/2008/09/28/making-feeds-friendly.aspx (英語) を参照してください)。ただし、すべての処理を行えるわけではありません (具体的には、Atom の "content" 要素をマップし直すことはできません)。WCF Data Services チームは、"わかりやすい" フィードであっても、さまざまなクライアント ライブラリで使用できるようにする必要があったためです。目標は、OData フィードを使いやすくすることであって、Atom や AtomPub を好んで使用して、OData を敬遠するということではありません。

ただし、このような場合は、OData ではなく、WCF Data Services を AtomPub エンドポイントとして使用します。そのためには、Atom と OData とをマッピングする必要があります (図 8 参照)。

Atom と OData のマッピング

図 8 Atom と OData のマッピング

問題は、このマッピングを実現する方法です。当然、データを取得しますが、Atom リーダー (およびライター) がデータの格納場所を把握できるように、そのデータを Atom プロパティにマップし直す必要があります。これを行う理由は、WCF Data Services は依然として .NET Framework の型へのマッピングを行うことができるためです。つまり、Entity Framework を通じてデータベースへのマッピングを行うことができるためです。ここで必要なのは、ごく簡単な Atom や AtomPub の OData へのマッピング、または 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;
    }
  }
}

このコードでは、"content" 要素内に入れ子になっている "properties" 要素を通じて、渡される Atom や AtomPub ("title"、"content"、"published" 要素など) を、対応する OData 形式にマッピングします。既定では、エンティティの名前が一致する場合 (大文字と小文字は区別しません)、マッピング (および強制型変換) が実行されます。たとえば、Title プロパティを含むエンティティ (今回の例の Post エンティティなど) が公開されるときに、Atom の "title" 要素にマップされます。

一方、自動マッピングが存在しない場合は、エンティティ名に基づく明示的なマッピングを使用して、マッピングの動作をオーバーライドできます。ここでは、"Posts" コレクションに含まれるオブジェクトの PublishDate プロパティを、Atom の "published" プロパティにマップしています。この 2 つの属性だけで、OData フィードを Atom フィードに変換でき、図 7 のようなすべての機能を備えたデータのビューを提供します。

このマッピングは一方向の操作ではありません。すべての HTTP メソッドをサポートしているので、AtomPub プロトコルを使用して、Posts コレクションの項目の作成、更新、削除、および読み取りを行うことができます。つまり、AtomPub をブログ API としてサポートしている Windows Live Writer (WLW) のようなツールを構成して、このツールをブログ投稿のリッチ テキスト編集に使用できます。たとえば、atompub.svc エンドポイントを指定したら、WLW で、[ブログ] メニューの [ブログ アカウントの追加] をクリックして表示されるダイアログ ボックスで、次のオプションを入力します。

  • どのブログ サービスを利用しますか?: 他のブログ サービス
  • ブログの Web アドレス: http://<サーバー>:<ポート>/atompub.svc
  • ユーザー名: <ユーザー名> (標準 HTTP の手法を使用して、AtomPub エンドポイントに実装する必要があります)
  • パスワード: <パスワード>
  • 使用するブログの種類: Atom Publishing Protocol
  • Service Document の: http://<サーバー>:<ポート>/atompub.svc
  • ブログのニックネーム: <自由に入力してください>

[完了] をクリックすると、ブログの投稿を管理するためのリッチ テキスト エディターが表示されます (図 9 参照)。

Atom と OData をマッピングして、ブログの投稿を管理するためのリッチ テキスト エディターを簡単に作成する

図 9 Atom と OData をマッピングして、ブログの投稿を管理するためのリッチ テキスト エディターを簡単に作成する

ここで、プロパティを Atom の "content" 要素に含めることで、完全な CRUD 機能をサポートする、WCF Data Services エンジンを使用します。そして、通常の従来からの Atom と AtomPub もサポートできるように、若干のマッピングも行います。

この作業を行うために使用した小さなサンプル ライブラリ (マイクロソフトの WCF Data Services チームのソフトウェア エンジニアである Phani Raj の協力を得て作成しました) は、必要最低限の機能しか備えていないため、これだけでは実際のブログを作成することはできません。Atom と AtomPub をサポートするために実際に必要な事項について思い浮かんだものを、次に一覧します。

  • 名前、URI、電子メールなど、Atom の author 要素のサブ要素のマッピング。
  • イメージの処理 (WLW では FTP を許可するため、これで十分な場合があります)。
  • WLW にこれらの機能を認識させる機能を公開する。

この処理をさらに追究することに関心がある場合は、WLW チームのメンバーである Joe Cheng が、WLW の AtomPub サポートに関する一連のブログ記事を作成しているので、まずはこの記事が作業に良い刺激を与えてくれます (ブログ記事については、jcheng.wordpress.com/2007/10/15/how-wlw-speaks-atompub-introduction (英語) を参照してください)。

それではお楽しみください。

Chris Sells は、マイクロソフトのビジネス プラットフォーム部門に所属するプログラム マネージャーです。さまざまな書籍を執筆しており、『Programming WPF』(O'Reilly Media、2007 年)、『Windows Forms 2.0 Programming』(Addison-Wesley Professional、2006 年)、『ATL インターナル: 仕組み・設計・実装法』(アスキー、1999 年) などの書籍の共著者でもあります。余暇には、さまざまなカンファレンスを催したり、マイクロソフト社内製品チームのディスカッション リストにいたずらをしたりしています。Sells と彼の各種プロジェクトの詳細については、sellsbrothers.com (英語) を参照してください。

この記事のレビューに協力してくれた技術スタッフの Pablo Castro に心より感謝いたします。