在30分钟内为StackOverflow创建一个包含XML 和JSON的OData API

[原文发表地址]  Creating an OData API for StackOverflow including XML and JSON in 30 minutes

[原文发表时间] 2010-03-28 17:29

昨晚我给Jeff Atwood发了一封只有一行的电子邮件。 “你应当使用Odata制作StackOverflow API。”然后我意识到了,正如Linus说的,“嘴上说很容易,请展示出你的代码”。因此在飞机上我使用Odata创建了StackOverflow API的一个最初的原型。我本来分配了整个12小时的飞行时间。不幸的是它只花了我30分钟,因此在剩余的时间里我在看电影。

你可以跟着(看)下去,并且如果你愿意的话可以自己亲手做。

准备工作

在我上飞机前,我下载了两个东西.

首先我下载了Sam Saffron的“So Slow” StackOverflow SQL Server Importer . 这是Sam的一个小spike,它从StackOverflow每月的转储中提取3gigs of XML Dump Files并把它输入到SQL Server里。

第二,我下载了StackOverflow Monthly Dump。我用uTorrent下载并解压它,以为飞行做准备。

输入到SQL Server

我进入Visual Studio 2010(尽管我本可以使用2008版本,我非常喜欢2010版本中的Entity Framework改进,它使这项工作更加地简单。)我右击了Server Explorer中的Data Connections节点并在SQL Express中创建了一个数据库,叫做,嗯,"StackOverflow。"

Create New SQL Server Database

接下来,在Visual Studio 中我从Sam的project里打开他的RecreateDB.sql文件(尽管可以,但我避免使用SQL Server Management Studio)并连接到".\SQLEXPRESS",选择新的StackOverflow数据库并点击“执行”。

Recreate DB SQL inside of Visual Studio

Sam的SQL文件的一个nit是,它虽然能创建很好地与转储排列在一起的表格,但它却不包含任何referential integrity。表格互不能识别且也没有基数设置。在没有Google Bing的前提下,我已经改写了那些我知道该怎么做的事情,我想我会稍侯再做处理。你也会这么做的。

接下来,我打开了Sam的SoSlow应用程序并运行它。它是一个漂亮的、如广告里那样的很小的应用程序,具有一个十分直观的用户界面。我很可能将“输入”按钮命名为如“放了那条猎犬!”一类的东西了,但是这就是我。

So Slow ... Stack Overflow database importer

在这里我有一个漂亮的几百兆大小的数据库,它装满了StackOverflow公共数据。

image

创建一个Web Project及一个Web Project

现在,从Visual Studio里,我选择了File | New Project | ASP.NET Web Application。然后我右击了随之生成的project并选择了Add | New Item,然后点击Data,ADO.NET Entity Data Model。

Add New Item - StackOveflow

那个又是怎么回事呢?Hanselman。你知道StackOverflow是使用LINQ to SQL的吗?你最终售罄了吗,并且是不是试图在这个巧妙伪装的博客帖子里把Entity Framework偷偷地强加给我们?

不。出于一些原因我使用了EF。首先,在Visual Studio 2010里它的速度足够快(运行时和设计时都快),以至于我都注意不到差别了。其二,我知道formal referential integrity的缺少将会是一个问题(还记得吗?我之前提到过它)并且LINQ to SQL是physical/logical 1:1 ,而EF能提供灵活的映射,我估计使用EF将更为简易。其三,"WCF Data Services"(这个data services之前称为ADO.NET Data Services 或 "Astoria")能够很好地映射到EF。

我把它命名为StackOverflowEntities.edmx并选取了"Update Model from Database", 然后选取了所有表格以便进行启动。当designer被打开后,我注意到并没有reference lines,只有独自存在的表格。

The Initial Entity Model

因此关于SQL Server中表格之间没有联系这一点我是对的。如果我更加聪明的话,我就应当把SQL衔接起来使之包含这些关系,但我想我仍可以在这里把它们添加上去以及其他一些可以使我们的OData Service用起来更加愉快的一些东西。

我首先查看帖子,有可能在看这个API里的一篇帖子,我想要看评论。因此,我右击了一篇帖子并点击了Add | Association。 那个对话框花了我一些时间来读懂(我以前从未见过它)然后我意识到它正在底部创建一个英语句子,因此我就只是关注使那个句子正确

在这种情况下,“帖子可以有*(许多)评论实例。使用Post.Comments来处理评论实例。评论可以有1(一)个帖子实例。使用Comment.Post来处理帖子实例。” 正是我想要的。同时我也有了外键属性,因此我未核取那个并点击了确定。

Add Association

在这儿--Designer里,难倒了我。注意带1...*的行,以及帖子上的Comments Navigation Property和评论上的Post Navigation Property。这都来自那个对话框。

Posts relate to Comments

接下来,考虑到我没有让它自动生成外键属性,我必须得自己映射它们。我双击了Association Line。我把Post选做Principal并把它的Id映射到Comments里的PostId属性上。

Referential Constraint

搞清楚了这个后,我就只为很明显的事情多次做了同样的事情,就像在这个图表中看到的一样:Users有Badges,Posts有Votes,等等。

A more complete StackOverflow Entity Model with associations completed

现在,让我们来做一个服务。

创建一个OData Service

在Solution Explorer中右击Project并选择Add | New Item | Web | WCF Data Service。 我把我的命名为Service.svc。要拥有一个完整的、工作的OData service在技术上你所要做的只是在尖括号(DataService<YourTypeHere>)间添加一个class,并为config.EntitySetAccessRule准备一行。这是我最初的最小的class。在我尝试获取所有的帖子后我添加了SetEntitySetPageSize。;)

    1: public class Service : DataService<StackOverflowEntities>
    2: {
    3:     // This method is called only once to initialize service-wide policies.
    4:     public static void InitializeService(DataServiceConfiguration config)
    5:     {
    6:         config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
    7:  
    8:                  //Set a reasonable paging site
    9:         config.SetEntitySetPageSize("*", 25);
   10:  
   11:                  config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
   12:     }
   13: }

扩展了这个class后,我添加了caching,以及一个Service Operation样本,还有WCF Data Services support for JSONP。请注意Service Operation只是在那儿的一个样本用来向StackOverflow显示它们可以有完全的控制。使用Odata并不意味着检查一个box并把你的数据库放到web上。它意味着可根据你的喜好以任意的间隔尺度来暴露具体的entities。你可以拦截查询,作出自定义行为(就像JSONP那个一样),创建定制Service Operations(当然,它们可以包含查询字符串),等等。Odata天生就支持JSON,并且当一个accept: header被设置后它就会返回JSON,但是我添加了JSONP support来允许跨域服务的使用以及允许URL里的格式参数,人工操作比较亲睐于这个,因为它更简单。

    1: namespace StackOveflow
    2: {
    3:     [JSONPSupportBehavior]
    4:     public class Service : DataService<StackOverflowEntities>
    5:     {
    6:         // This method is called only once to initialize service-wide policies.
    7:         public static void InitializeService(DataServiceConfiguration config)
    8:         {
    9:             config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
   10:             
   11:             //This could be "*" and could also be ReadSingle, etc, etc.
   12:             config.SetServiceOperationAccessRule("GetPopularPosts", ServiceOperationRights.AllRead);
   13:             
   14:             //Set a reasonable paging site
   15:             config.SetEntitySetPageSize("*", 25);
   16:             
   17:             config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
   18:         }
   19:  
   20:         protected override void OnStartProcessingRequest(ProcessRequestArgs args)
   21:         {
   22:             base.OnStartProcessingRequest(args);
   23:             //Cache for a minute based on querystring
   24:             HttpContext context = HttpContext.Current;
   25:             HttpCachePolicy c = HttpContext.Current.Response.Cache;
   26:             c.SetCacheability(HttpCacheability.ServerAndPrivate);
   27:             c.SetExpires(HttpContext.Current.Timestamp.AddSeconds(60));
   28:             c.VaryByHeaders["Accept"] = true;
   29:             c.VaryByHeaders["Accept-Charset"] = true;
   30:             c.VaryByHeaders["Accept-Encoding"] = true;
   31:             c.VaryByParams["*"] = true;
   32:         }
   33:  
   34:         [WebGet]
   35:         public IQueryable<Post> GetPopularPosts()
   36:         {
   37:             var popularPosts = (from p in this.CurrentDataSource.Posts 
   38:                                orderby p.ViewCount
   39:                                select p).Take(20);
   40:  
   41:             return popularPosts;
   42:         }
   43:     }
   44: }

但是这使我们得到什么呢?又怎么样呢

用Odata处理StackOverflow的数据

恩,如果我点击<https://mysite/service.svc我就会看到这项服务。请注意相关的HREFs>。

Screenshot of an XML document describing an OData service endpoint

如果我点击https://173.46.159.103/service.svc/Posts我就会得到这些帖子(分成页的,就像我已经提到的那样)。请看其中关键的地方。注意到内容前面的<link>了吗?注意到相关的href="Posts(23)"了吗?

StackOverflow Posts in OData

还记得我之前设置的所有那些联系吗?现在我可以看到:

但那只是导航。我还可以做查询。 去下载LINQPad Beta for .NET 4。点击Add Connection并放进我的小Add Connection server里去。

免责声明:这是一个测试server—Orcsweb随时可能出问题。同时请注意,你也可以在https://www.vs2010host.com注册你自己的或者在ASP.NET里找到一个主机或者在cloud里host你自己的OData。

我把这个放了进去并点击确定。

LINQPad Connection String

现在我正在网上根据StackOverflow编写LINQ queries。没有任何Twitter式的API,JSON或其它什么东西可以做这个。StackOverflow是为StackOverflow准备的。我对这鼓弄的越多,越加意识到这是真的。

LINQPad 4

这个LINQ query事实上转换成了这个URL。同样,你不需要.NET,它只是HTTP:

',Tags)">',Tags)">https://173.46.159.103/service.svc/Posts()?$filter=substringof('SQL',Title) or substringof('<sql-server>',Tags)

用accept: application/json的一个accept header来尝试同样的事情或者仅仅添加 $format=json

',Tags)&$format=json">',Tags)&$format=json">https://173.46.159.103/service.svc/Posts()?$filter=substringof('SQL',Title) or substringof('<sql-server>',Tags)&$format=json

它就会自动返回与JSON 或 Atom同样的数据,如你喜欢的那样。

如果你已经有了Visual Studio,只需快速创建一个控制台应用程序. File | New Console App,然后右击references并点击Add Service Reference。放进https://173.46.159.103/service.svc里并点击确定。

Add Service Reference

尝试一些诸如这样的东西。我把URIs放进评论里以证明并没有什么欺骗。

    1: class Program
    2: {
    3:     static void Main(string[] args)
    4:     {
    5:         StackOverflowEntities so = new StackOverflowEntities(new Uri("https://173.46.159.103/service.svc"));
    6:  
    7:         //{https://173.46.159.103/service.svc/Users()?$filter=substringof('Hanselman',DisplayName)}
    8:         var user = from u in so.Users
    9:             where u.DisplayName.Contains("Hanselman")
   10:             select u;
   11:  
   12:         //{https://173.46.159.103/service.svc/Posts()?$filter=OwnerUserId eq 209}
   13:         var posts =
   14:             from p in so.Posts
   15:             where p.OwnerUserId == user.Single().Id
   16:             select p;
   17:  
   18:         foreach (Post p in posts)
   19:         {
   20:             Console.WriteLine(p.Body);
   21:         }
   22:  
   23:         Console.ReadLine();
   24:     }
   25: }

我可以用PHP, JavaScript等里面的例子继续下去,但是你已经明白了。

结语

StackOverflow在他们的数据上一直都是非常地开放、量大。我建议OData endpint应当比一个定制OData endpint和/或JSON API给予我们更大的灵活性来处理它们的数据,因此它们需要不断被还原。

有了专有的API,人们将急于创建多种语言的StackOverflow clients,但是这项工作已经用OData完成了,包括iPhone, PHP 和 Java的libraries。可被用作向这样的一项服务发起对话的OData SDKs正在不断地增多。如果我愿意的话我也可以使用PowerPivot把它载入Excel里去。

此外,这项服务可以完全被扩展至这个简单的GET例子以外。你可以用OData来做完整的CRUD,它不以任何方式绑向.NET。或许是TweetDeck for StackOverflow?

我建议我们鼓励把比30分钟更多的时间(我之前已经投入进去的)投入到StackOverflow,并为它们的数据提供一个合适的OData服务,而不是一个定制的API。我自愿提供帮助。如果不这样的话,我们可以用他们的转储数据(如果他们能加点紧,或许能每周一次)以及一个云实例来自己做。

有什么想法吗?