OData 和 AtomPub

使用 WCF 数据服务构建 AtomPub 服务器

Chris Sells

下载代码示例

如果您不熟悉开放数据协议 (OData),我要告诉您它很美妙。OData(在 odata.org 上有详细介绍)以下列各种基于 HTTP 的功能优势为基础:用于发布数据的 Atom;用于创建、更新和删除数据的 AtomPub;以及用于定义数据类型的 Microsoft 实体数据模型 (EDM)。

如果您拥有 JavaScript 客户端,则可以采用 JSON 格式(而不是 Atom 格式)直接返回数据;如果您拥有其他客户端(包括 Excel、.Microsoft NET Framework、PHP、AJAX 等),则可以使用客户端库来形成 OData 请求和处理 OData 响应。如果您在服务器端使用 .NET Framework,则 Microsoft 还提供一个易于使用的库,该库称为 WCF 数据服务,用于公开 Microsoft 实体框架支持的 .NET Framework 类型或数据库作为 OData 源。这样,您就可以采用基于 HTTP 的方式和标准方式,通过 Internet 轻松公开您的数据。

话虽如此,您也可能希望使用 OData 执行一些并不完全属于现成功能的任务,如将 OData 与现有基于 Atom 和 AtomPub 的阅读器和编写器集成。这正是我们要尝试的功能。

一个简单的博客

例如,假设我要构建一个简单的博客系统(事实上,此工作基于我对 sellsbrothers.com 上的内容管理系统的重新编写)。我对 Visual Studio 2010 中的模型优先支持十分着迷,因此创建了一个 ASP.NET MVC 2.0 项目,添加了一个名为 MyBlogDB.edmx 的 ADO.NET EDM 文件,并设计了一个 Post 实体,如图 1 所示。

图 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),然后在出现提示时单击“确定”以进行创建。创建数据库以后,可以在服务器资源管理器中,在 Visual Studio 刚刚为您创建的连接下浏览该数据库。

为了方便测试,可以在表中直接添加数据,方法是右键单击 Posts 并选择“显示表数据”,这会显示一个小网格,如图 3 所示。

图 3**“显示表数据”网格可方便测试**

这虽然不是世界上最好的编辑体验,但是要好过通过编写 SQL 语句最终得到并运行端到端编辑解决方案(即将讨论这种方法,请往下读!)。

我们已经有了一些数据,现在便可以通过更新 HomeController.cs 来编写一些 ASP.NET 代码,从而显示这些数据(请在 asp.net/mvc/ 阅读有关 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 类的一个实例,该类是从 MyBlogDB.edmx 文件创建的顶级 ObjectContext 派生类,我们通过它来访问新数据库。 (如果您不熟悉实体框架,请参见 msdn.com/data/aa937723)。在对 HomeController 类调用 Index 方法时,表示有人正请求我们的新 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>

在这里,我们将基类更改为采用生成的 Post 类型集合(以及 MyBlogDBContainer 类)来对 Posts 表进行建模。我们还将主页内容替换为一个 foreach 语句,以显示每篇文章的标题、内容和发布日期。

这就是我们需要的全部操作。现在,当我们执行该项目(“调试”|“启动调试”)时,浏览器会启动并显示博客文章(如果您没有在数据库中放入更多文章,那么就只有一篇文章),如图 4 所示。

图 4 完成的网页

在我告诉您所有这些之后,现在我可以这样跟您说:OData 的神奇之处在于,只需几下简单的鼠标和键盘操作,我便可以公开这些数据的完整编程接口,从而可以通过 JavaScript、.NET Framework、PHP 等访问这些数据。若要亲眼见证这种神奇,请在解决方案资源管理器中右键单击您的项目,选择“添加”|“新建项”,选择“WCF 数据服务”,选择一个名称(我使用 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 数据服务的核心(请参见 msdn.com/data/bb931106)。 通过在 OData 协议中定义的基于 HTTP 谓词的创建、读取、更新和删除 (CRUD) 操作,可以使用 DataService 类轻松公开我们的数据库。 对于传递给 DataService 的类型,将检查其用于公开集合的公共属性。 在我们的示例中,实体框架生成的对象上下文类包含非常符合要求的 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 的 ObjectSet(只是一种集合),其中包含 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) 定义。 更深入一层,可以看到我们的博客文章在 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>

这个文件几乎是一个彻头彻尾的传统 Atom (ietf.org/rfc/rfc4287.txt),唯一不同的就是基于 Microsoft 的 URI,这些 URI 用于将 OData 功能分层到 Atom 之中。具体而言,您需要注意“content”元素中的“properties”元素。您会看出这些属性就是以前在 Post 实体和相应 Posts 表中定义的相同属性。此数据包含在由 Atom 定义并通过 CRUD 注释公开的信封之中,这些注释本身由 AtomPub 定义,
使您可以通过 HTTP 方法 POST、GET、PUT 和 DELETE 分别执行创建、读取、更新和删除操作。问题在于这不是完全传统的 Atom。例如,如果我们在 Atom 阅读器(如 Internet Explorer 8)中浏览至 odata.svc/Posts,则标题和内容不会正确显示,如图 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”元素包含的信息过去隐藏在“content”元素中 OData“properties”元素的 Title 属性之中,“content”元素已由 Content 属性覆盖,并且已从 PublishDate 属性的值添加了“published”元素。当在 Internet Explorer 中查看此数据时,我们获得的信息更加符合我们的期望,如图 7 所示。

图 7 调整 XML 格式可正确显示标题和内容

应该指出,这只用于支持我们关心的博客工具。Internet Explorer 不期望获取客户列表或发票;它期望获取标题和发布日期以及 HTML 内容。对客户列表和发票进行这种映射有时十分有用,Microsoft 在 WCF 数据服务中的一项名为“友好源”的功能就是一例(请参见 blogs.msdn.com/astoriateam/archive/ 2008/09/28/making-feeds-friendly.aspx)。然而,该功能并不能执行所有操作(具体而言,它不会重新映射 Atom“content”元素),因为 WCF 数据服务团队希望确保即使是“友好”源也仍然可以使用各种客户端库。这样做的目标是使 OData 源更加友好,而不是放弃 OData 以支持 Atom/AtomPub。

然而在本例中,我们放弃 OData,只使用 WCF 数据服务作为 AtomPub 终结点,这要求在 Atom 与 OData 之间进行映射,如图 8 所示。

图 8 Atom 与 OData 之间的映射

奥妙在于:如何进行这种映射?我们显然已获取了数据,但是需要将其重新映射到 Atom 属性,以便 Atom 阅读器(和编写器)知道存放数据的位置。这样做的原因是使 WCF 数据服务仍可以对 .NET Framework 类型进行映射,或通过实体框架对数据库进行映射。我们要做的只是在 Atom/AtomPub 与 OData 之间进行一点简单的映射。

本文附带的代码示例将一些代码注入 WCF 管道,该管道恰好允许进行这种消息数据转换。您可以将它读入您的核心内容(请查看 ODataBlogging.cs),但我要介绍的是如何使用它。

首先,像以前一样新建一个 WCF 数据服务终结点,但使用不同的名称(我使用 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 属性映射到“published”atom 属性时就是这样做的。通过这两个特性足以将我们的 OData 源转换为 Atom 源,从而为我们提供功能完整的数据视图,如图 7 所示。

此映射不是单向的;它支持所有 HTTP 方法,因此可以使用 AtomPub 协议在 Posts 集合中创建、更新和删除项以及读取这些项。这表示您可以配置 Windows Live Writer (WLW) 之类的工具(该工具支持 AtomPub 作为博客 API),并使用该工具对文章进行丰富的文本编辑。例如,如果提供了 atompub.svc 终结点,则在 WLW 中,您可以选择“日志”|“添加日志帐户”,然后在随后出现的对话框中填写以下选项:

  • 您使用哪种日志服务?其他日志服务
  • 您的日志网址:http://<<服务器>>:<<端口>>/atompub.svc
  • 用户名:<<用户名>>(必填项,应使用标准 HTTP 方法在 AtomPub 终结点上实现)
  • 密码:<<密码>>
  • 要使用的日志类型:Atom 发布协议
  • 服务文档 URL:http://<<服务器>>:<<端口>>/­atompub.svc
  • 博客昵称:<<您喜欢的任何名称>>

单击“完成”,会向您显示一个用于管理博客文章的 RTF 编辑器,如图 9 所示。

图 9 Atom/OData 映射有助于构建 RTF 编辑器以管理您的博客文章

我们在这里采用了数据服务引擎(该引擎通过将属性打包到 Atom“content”元素中来支持完整的 CRUD 功能),并进行了一些映射,以使其也支持传统的 Atom 和 AtomPub。

我用于撰写本文的小型示例库(是我同 Phani Raj 一起创作的,他是 Microsoft WCF 数据服务团队的一名软件工程师)只具备最简单的功能,构建真正的博客所需的工作远不止这么简单。下面列出了我首先想到的、在实际支持 Atom 和 AtomPub 时仍需要进行的一些事项:

  • 映射 Atom 作者元素子元素,如姓名、URI 和电子邮件。
  • 处理图像(不过 WLW 允许使用 FTP,这样可能就足够了)。
  • 公开功能以使 WLW 可识别这些功能。

如果您有兴趣深入进行这方面的尝试,可以参考 Joe Cheng(WLW 团队成员)撰写的一系列有关 WLW 中的 AtomPub 支持的博客文章,本文的灵感最开始便是来自这些文章:jcheng.wordpress.com/2007/10/15/how-wlw-speaks-atompub-introduction

请尽情体验吧!

Chris Sells 是 Microsoft 业务平台部门的项目经理。他有几本著作,包括与他人合著的《Programming WPF》(O’Reilly Media,2007 年)、《Windows Forms 2.0 Programming》(Addison-Wesley Professional,2006 年)和《ATL Internals》(Addison-Wesley Professional,1999 年)。在业余时间,他主持各种会议,并且是 Microsoft 内部产品团队讨论列表上的常客。sellsbrothers.com 上提供了有关 Sells 和他的各种项目的详细信息。

*衷心感谢以下技术专家对本文的审阅:*Pablo Castro