Создание OData API для StackOverflow включая XML и JSON за 30 минут

Автор: Скотт Хансельман
Статья: Creating an OData API for StackOverflow including XML and JSON in 30 minutes

Прошлой ночью я отправил письмо Jeff Atwood в котором была одна строка. «Ты должен сделать Stackoverflow API используя OData». Then I realized that, as Linus says, Talk is Cheap, Show me the Code. Я отвел под это весь 12 часовой полет. Но к сожалению это заняло 30 минут, так что остальное время я смотрел фильмы.

Вы можете следить за процессом и делать это самостоятельно, если хотите.

Подготовка

Перед моим полетом я сделал две вещи.
Во-первых, я скачал утилиту импорта Сэма Саффрона «So Slow» Stackoverflow SQL Server. Это маленькая утилита Сэма, которая берет 3х Гб файл XML ежемесячного дампа Stackoverflow и импортирует его в SQL Server.

Во-вторых, я скачал ежемесячный дамп Stackoverflow. Я скачал его с помощью uTorrent и распаковал его во время подготовки к полету.

Импорт в SQL Server

Я переключился в Visual Studio 2010 (хотя мог бы использовать 2008, но мне понравились улучшения в Entity Framework в 2010, которые позволяют делать работу проще). Правым кликом мыши на узле Data Connections в Server Explorer я создаю базу данных в SQL Express с именем, гмм, «Stackoverflow».

Далее, я открыл в Visual Studio файл Сэма RecreateDB.sql из его проекта (не пользуюсь SQL Server Management Studio, если это возможно) подключился к экземпляру «.\SQLEXPRESS», выбрал новую базу StackOverflow и нажал «Execute».

One nit (( One nit )) по поводу SQL файла Сэма, он красиво создает таблицы из дампа, но он не содержит информации о ссылочной целострости. Таблицы никак не связаны с другими и они не имеют cardinality setup (( cardinality setup)). Я перезаписал клетки головного мозга которые знали что с этим делать без Google и Bing, поэтому я решил вернуться к этому позже. И вы тоже.

Далее, я открыл приложение Сэма SoSlow и запустил его. Это мелкое красивое приложение с интуитивным интерфейсом, которое работает так как и обещалось. Как по мне, я бы, наверное назвал кнопку «Import» чем то вроде «Release the Hounds!».

На данный момент я имею базу данных с несколькими сотнями мегабайт публичных данных Stackoverflow.

Создание Веб проекта и Модели сущностей

Итак, я выбрал в Visual Studio File | New Project | ASP.NET Web Application. Затем, я правым кликом на исходном проекте выбрал Add | New Item, then clicked Data, then ADO.NET Entity Data Model.

Что делать с этим, Хансельман? Вы знаете что Stackoverflow использует LINQ to SQL? Вы наконец продались и скрытно пытаетесь заставить нас использовать Entity Framework с помощью этого замаскированного сообщения?

Нет. Я использую EF по нескольким причинам. Во-первых, в Visual Studio 2010 он настолько быстрый (и во время выполнения (runtime) и во время проектирования (design time) ), что я больше не замечаю разницы. Во-вторых, я знал что отсутствие ссылочной целостности приведет к проблеме (помните, я говорил об этом ранее?) и так как маппинг в LINQ to SQL 1:1 (( LINQ to SQL is 1:1 physical/logical )), а EF предлагает гибкий маппинг, я понял что будет легче сделать это с EF. В-третьих, «WCF Data Services» (сервисы данных ранее известные как ADO.NET Data Services или «Astoria») отлично маппяться на EF.

Я назвал файл StackOverflowEntities.edmx и нажал «Update Model from Database» и для начала выбрал все таблицы. Когда открыл дизайнер, я заметил что нет линий связи, только таблицы.

Итак, я оказался прав насчет того что между таблицами в SQL Server небыло связей. Я бы подключился к SQL Server`у для добавления этих связей, но подумал что могу добавить их здесь так же хорошо как и другие вещи которые сделают нашу работу с OData Service более приятной.

Я начал просматривать таблицу сообщений (Posts) и представлять что если я смотрю на сообщения, то захочу видеть комментарии (Comments). Поэтому я щелкнул правой кнопкой мыши на таблице Posts и выбрал Add | Association. Потратив секунду на понимание появившегося диалога (я никогда не видел его ранее), я понял, что он делает, то о чем написано в предложении снизу, поэтому я сосредоточился чтобы это предложение было верным. В данном случае, «Post can have * (Many) instances of Comment. Use Post.Comments to access the Comment instances. Comment can have 1 (One) instance of Post. Use Comment.Post to access the Post instance. (( Сообщение может иметь много комментариев. Используйте Post.Comments для доступа к коментариям. Коментарий может иметь один экземпляр Сообщения. Используйте Comment.Post для доступа к экземпляру Сообщения )) то что я хотел. Также, у меня были свойства с внешними ключами, поэтому я отключил их и нажал кнопку OK.

Это привело меня в дизайнер. Обратите внимание на линию с текстом 1..* и на свойства навигации: Comments в классе Post и Post в классе Comment. Все это появилось из прошлого диалога.

Потом, после того как я понял что у меня нет внешнего ключа в виде автоматического свойства, и я должен замаппить его сам. Дважды щелкнул на линию ассоциации. Выбрал класс Post как главный элемент (Principal) и связал его свойство Id с свойством PostId класса Comments.

Поняв это, я просто создал все очевидные связи для таблиц, которые видны на диаграмме, где Users (Пользователи) имеют Badges (идентификационные данные), Posts (сообщения) имеют Votes (голоса), и так далее.

Теперь, давайте создадим сервис.

Создание OData сервиса

Нажав правой кнопкой мыши на проект в Solution Explorer выбрал Add | New Item | Web | WCF Data Service. Я назвал его Service.svc. Все что вам необходимо для создания полноценного работающего OData сервиса это добавить класс между угловыми скобками (DataService) и добавить код одну строку config.EntitySetAccessRule. Вот мой исходный класс. Я добавил SetEntitySetPageSize, после того как я попытался получить все сообщения. ;-)
[CSharp]
public class Service : DataService
{
// Этот метод вызывается только один раз для инициализации всех политик сервиса.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule(«*», EntitySetRights.AllRead);

//Устанавливаем приемлемый размер страницы
config.SetEntitySetPageSize(«*», 25);

config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
[/CSharp]
Расширяя этот класс, я добавил кеширование, и пример операции сервиса, так же как WCF Data Services support for JSONP. Обратите внимание что операция сервиса это лишь пример чтобы показать что StackOverflow может иметь полный контроль (( to show StackOverflow that they CAN have total control )). Использование OData не означает установить флажок и вывести свою базу данных в сеть. Это подразумевает выделение ограниченного количества сущностей с большой или малой детализацией, так как нравится вам. Вы можете перехватывать запросы, создавать настраиваемые поведение (например такое как JSONP), создавая настраиваемые операции сервиса (они могут содержать строки запросов, конечно же), и многое другое. OData нативно поддерживает формат JSON, и будет возвращать JSON если заголовок установлен, но я добавил поддержку JSONP что бы позволить междоменное использование сервиса, а так же форматировать параметры в URL, потому что так понятнее и легче для человека.
[CSharp]
namespace StackOveflow
{
[JSONPSupportBehavior]
public class Service : DataService
{
// Этот метод будет вызван один раз для инициализации всех политик сервиса.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule(«*», EntitySetRights.AllRead);

// Здесь могло бы быть «*» и так же ReadSingle, и тд.
config.SetServiceOperationAccessRule(«GetPopularPosts», ServiceOperationRights.AllRead);

// Установим разумный размер страницы
config.SetEntitySetPageSize(«*», 25);

config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}

protected override void OnStartProcessingRequest(ProcessRequestArgs args)
{
base.OnStartProcessingRequest(args);
// Минутный кеш, на основе строки запроса
HttpContext context = HttpContext.Current;
HttpCachePolicy c = HttpContext.Current.Response.Cache;
c.SetCacheability(HttpCacheability.ServerAndPrivate);
c.SetExpires(HttpContext.Current.Timestamp.AddSeconds(60));
c.VaryByHeaders[«Accept»] = true;
c.VaryByHeaders[«Accept-Charset»] = true;
c.VaryByHeaders[«Accept-Encoding»] = true;
c.VaryByParams[«*»] = true;
}

[WebGet]
public IQueryable GetPopularPosts()
{
var popularPosts = (from p in this.CurrentDataSource.Posts
orderby p.ViewCount
select p).Take(20);

return popularPosts;
}
}
}
[/CSharp]
Но что это нам дало?

Доступ к данным StackOverflow через OData

Хорошо, если я открою http://mysite/service.svc Я увижу этот сервис. Обратите внимание на относительные ссылки (HREFs).

Если я открою http://173.46.159.103/service.svc/Posts Я получу сообщения (постраничные, как я упоминал). Посмотрите на это более детально. Обратили внимание на содержимое тегов до содержимого? Обратили внимание на атрибут href с относительной ссылкой href=»Posts(23)»?

Помните все те связи которые Я настроил ранее? Теперь Я могу посмотреть:

Но это все только навигация. Я также могу делать запросы. Скачайте LINQPad Beta for .NET 4. Peep this. Нажмите на Add Connection, и укажите мой маленький тестовый сервер Orcsweb.

Оговорка: Это тестовый сервер Orcsweb который может остановиться в любой момент. Так же отметим, что вы можете зарегистрироваться на http://www.vs2010host.com/ и получить свой сервер или найти ASP.NET хостинг или разместить ваш OData сервис в облаке.

Я ввел это и нажал OK.

Теперь я пишу LINQ запросы для StackOverflow через сеть. Нет Twitter подобного API, JSON так или иначе может сделать это. Данные StackOverflow предназначены для OData. Чем больше я вожусь с ними тем больше понимаю что это верно.

На самом деле этот LINQ запрос можно преобразовать в URL. Кроме того для этого вам не нужен .NET, только HTTP:
http://173.46.159.103/service.svc/Posts()?$filter=substringof(‘SQL’,Title) or substringof(‘<sql-server>’,Tags)
Попробуйте то же самое с заголовком: application/json или просто добавьте к запросу параметр $format=json
http://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, потом правым щелчком мыши добавьте Add Service Reference. Добавьте туда http://173.46.159.103/service.svc и нажмите кнопку OK.

Попробуйте что нибудь подобное. Я добавил в комментарии URI, что бы показать вам что тут нет обмана.
[CSharp]
class Program
{
static void Main(string[] args)
{
StackOverflowEntities so = new StackOverflowEntities(new Uri(«http://173.46.159.103/service.svc»));

//{http://173.46.159.103/service.svc/Users()?$filter=substringof(‘Hanselman’,DisplayName)}
var user = from u in so.Users
where u.DisplayName.Contains(«Hanselman»)
select u;

//{http://173.46.159.103/service.svc/Posts()?$filter=OwnerUserId eq 209}
var posts =
from p in so.Posts
where p.OwnerUserId == user.Single().Id
select p;

foreach (Post p in posts)
{
Console.WriteLine(p.Body);
}

Console.ReadLine();
}
}
[/CSharp]
Я мог бы продолжить с примерами на PHP, JavaScript, и так далее, но поставлю на этом
точку.

Заключение

StackOverflow всегда был невероятно открыт и щедрый на свои данные. Предположу что OData предложит нам гораздо более гибкий доступ к своим данным чем настраиваемый XML и/или JSON API которые должны быть постоянно корректироваться.
С проприетарным API, люди кинуться создавать клиентов для StackOverflow на разные языках, но эта работа уже сделана с помощью OData включая библиотеки для iPhone и Java. Вот растущий список OData SDK которые могут быть использованы для взаимодействия с подобными сервисами. Я могу загрузить данные в Excel с помощью PowerPivot, если мне это понравится.

Так же, сервис может быть полностью расширен за пределы этого простого GET запроса. Вы можете полностью реализовать CRUD операции с помощью OData и оно ни коим образом не будет связана с .NET. Может быть TweetDeck для StackOverflow?

Я надеюсь мы воодушевим StackOverflow потратить больше чем 30 минут, которые я потратил на это, и сделать достойный OData сервис для своих данных, чем тратить время на создание специализированного API. Я добровольно помогу в этом. Если нет, то мы можем сделать это сами с дампом их данных (еженедельным, надеюсь они могут это ускорить?) и облачным экземпляром.

Вопросы?