WCF + REST

Я уже рассказывал о том, что для построения веб-сервисов, кроме наиболее популярного подхода SOAP, существует другой подход – REST. Очень удачной для реализации REST-сервисов является технология ADO.NET Data Services. Однако, для построения REST-сервисов мы также можем воспользоваться и Windows Communication Foundation. Давайте посмотрим каким образом.

Прежде чем рассматривать инструменты для построения REST-сервисов WCF, давайте вспомним отличительные черты таких сервисов.

Любой веб-сервис на базе REST:

  1. Позволяет через Web (а именно поверх HTTP/HTTPS) предоставить доступ к данным;
  2. Каждый блок некоторой информации, предоставляемый через REST-сервис однозначно адресуется с помощью URI;
  3. Результатом работы такого сервиса будет некий XML документ (RSS, Atom, ..), т.е. feed и вспомогательные данные (бинарные, html, все что угодно).

Если к 1) и 3) пункту вопросов не возникает, то видно что 2) пункт явно не укладывается в идеологию WCF в том виде, в котором она существовала ранее. Т.е. если ранее мы могли как-то адресовать веб-сервисы, то на каждый URI нужно было описать публичный контракт и создать сервис. Однако, для REST-сервисов это критичный момент. Поэтому для реализации такой поддержки в составе WCF .NET Framework 3.5 появился целый HTTP API, который призван решить эти и другие проблемы.

В рамках HTTP API существуют такие важные классы как UriTemplate, UriTemplateMatch и UriTemplateTable. Именно этот набор классов позволяет осуществить маппинг конкретных URI на нужные операции. Несмотря на то, что эти объекты напрямую мы не используем, я думаю будет очень полезно для общего понимания посмотреть как они работают. Для этого давайте посмотрим следующий пример:

Uri address = new Uri("http://localhost:8000");
UriTemplate template = new UriTemplate("{category}/{product}");
Uri boundUri = template.BindByPosition(address, "notebooks", "lenovo-t61");
Console.WriteLine(address.ToString()); // http://localhost:8000/notebooks/lenovo-t61

Как видно, здесь мы задаем некий базовый URI и шаблон URI. Далее, используя полученный шаблон можно задать значение секций, которые определены в шаблоне. Как видим, в результате мы получаем URI вида http://localhost:8000/notebooks/lenovo-r61. Понятно, что если мы просто изменим шаблон URI, например, на shop/{category}/{product}/info, то в результате получим http://localhost:8000/shop/notebooks/lenovo-r61/info” И так далее. Дальше, используя класс UriTemplateMatch можно выполнить обратную операцию – по полученному URI и шаблону получить значение конкретных секций (в данном примере - category, product).

Как я уже говорил, эти объекты мы не используем напрямую, тем не менее они используются при маппинге URI на конкретную операцию.

Теперь, когда мы знаем как осуществляется маппинг, давайте рассмотрим каким образом можно построить REST-сервис на базе WCF. Для этого очень удобно пользоваться стандартным шаблоном при созданни проекта в VS2008 Syndication Service Library. Конечно, это не обязательное условие.

Построение публичного контракта. Для создания любого WCF-сервиса в первую очередь необходимо создать для него публичный контракт. REST-сервисы на базе WCF не исключение. Стандартный контракт в WCF для REST-сервиса выглядит следующим образом:

[ServiceContract]
[ServiceKnownType(typeof(Atom10FeedFormatter))]
[ServiceKnownType(typeof(Rss20FeedFormatter))]
public interface IFeed1
{
    [OperationContract]
    [WebGet(UriTemplate = "*")]
    SyndicationFeedFormatter CreateFeed();

    // TODO: Add your service operations here
}

В таком контракте, кроме стандартных атрибутов ServiceContract и OperationContract мы видим одно нововведение – атрибут WebGet. Именно с помощью этого атрибута мы можем указать какой URI соответствует какой операции. В приведенном примере все URI отображаются на единственную операцию — CreateFeed(). Давайте этот пример несколько модифицируем:

[ServiceContract]
[ServiceKnownType(typeof(Atom10FeedFormatter))]
[ServiceKnownType(typeof(Rss20FeedFormatter))]
public interface IFeed1
{
    [OperationContract]
    [WebGet(UriTemplate = "authors")]
    SyndicationFeedFormatter AuthorsFeed();

    [WebGet(UriTemplate = "authors/{author}")]
    SyndicationFeedFormatter AuthorsFeed(string author);
}

Теперь, в таком сервисе мы можем использовать два вида URI: ../feed1/authors и ../feed1/authors/<имя автора>. Причем при вызове того или иного URI, WCF автоматически определит какую операцию нужно вызвать, а во втором случае еще и передаст нужные параметры в метод (которые извлекаются из самого URI, помним объект UriTemplateMatch из HTTP API). Подобным образом мы можем создать нужное количество операций и определить для них те URI, которые нам необходимы.

Теперь давайте обратим внимание на тип возвращаемого операцией значения. В данном случае мы возвращаем некую ленту (feed). В поставке .NET 3.5 существуют два стандартных форматировщика – для RSS 2.0 и AtomPub 1.0. Однако, никто не мешает нам написать свои собственные форматировщики :).

Я уже говорил о том, что в рамках такого сервиса мы можем возвращать не только XML, но и вообще говоря любые данные, например, картинку. Для этого мы можем изменить контракт следующим образом:

[ServiceContract]
[ServiceKnownType(typeof(Atom10FeedFormatter))]
[ServiceKnownType(typeof(Rss20FeedFormatter))]
public interface IFeed1
{
    [OperationContract]
    [WebGet(UriTemplate = "authors")]
    SyndicationFeedFormatter AuthorsFeed();

    [WebGet(UriTemplate = "authors/{author}")]
    SyndicationFeedFormatter AuthorsFeed(string author);

    [WebGet(UriTemplate = "authors/photo/{author}")]
    Stream AuthorPhoto(string author);
}

В данном случае мы видим, что добавилась еще одна операция со своим URI, которая возвращает Stream. Внутри этого потока может быть что угодно, в т.ч. картинка.

Контракт готов, давайте перейдем к реализации самого сервиса. Собственно, сам сервис – это класс, реализующий наш публичный контракт. Здесь самое время поговорить об объектах SyndicationFeed и SyndicationItem – это объекты, являющются абстракциями ленты (feed) и ее содержимого. Наличие таких объектов позволяет нам не думать о конкретном формате ленты (RSS, Atom или еще что), а просто создать содержимое нашей ленты. Таким образом, метод AuthorsFeed() может выглядеть следующим образом:

public SyndicationFeedFormatter AuthorsFeed()
{
    SyndicationFeed feed = new SyndicationFeed("Feed Title", "A WCF Syndication Feed", null);

    List<SyndicationItem> items = new List<SyndicationItem>();
    items.Add(new SyndicationItem("Author1", "Author1. Age: 23.", null));
    items.Add(new SyndicationItem("Author2", "Author2. Age: 26.", null));

    feed.Items = items;
    
    return new Rss20FeedFormatter(feed);
}

Как видно, здесь мы создаем ленту, наполняем ее содержимым и возвращаем конкретный форматировщик – в данном случае форматировщик RSS 2.0. Если мы хотим работать с AtomPub, можно несколько изменить реализацию метода:

public SyndicationFeedFormatter AuthorsFeed()
{
    SyndicationFeed feed = new SyndicationFeed("Feed Title", "A WCF Syndication Feed", null);

    // наполнение ленты содержимым

    return new Atom10FeedFormatter(feed);
}

Поскольку при реализации REST-сервиса мы имеем дело с HTTP, то мы можем оперировать с различными HTTP-сущностями, например HTTP-заголовками или GET-параметрами. Сделать это можно, используя объект WebOperationContext, который также входит в состав HTTP API. Таким образом, мы можем на основе значения GET-параметра выбирать нужный форматировщик:

public SyndicationFeedFormatter AuthorsFeed()
{
    SyndicationFeed feed = new SyndicationFeed("Feed Title", "A WCF Syndication Feed", null);

    // ...

    string query = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters["format"];

    if (query == "atom")
    {
        return new Atom10FeedFormatter(feed);
    }
    else
    {
        return new Rss20FeedFormatter(feed);
    }
}

Точно также можно работать, например с HTTP-заголовками.

Однако, в рамках нашего REST-сервиса мы можем возвращать не только ленту, но и, как я уже упоминал, любые другие данные. Например изображение:

public Stream AuthorPhoto(string author)
{
    MemoryStream result = new MemoryStream();

    // заполнение потока

    WebOperationContext.Current.OutgoingResponse.ContentType = "image/jpeg";

    return result;
}

Обратите внимание, в данном случае кроме того, что мы возвратили поток нужно установить значение HTTP-заголовка Content-Type. Это можно осуществить используя объект WebOperationContext. Подобным образом можно вернуть, например, некий html-код или что-то еще.

Мы посмотрели на то, каким образом мы можем предоставить доступ к данным в рамках нашего REST-сервиса. Однако, REST-подход предполагает не только получение данных, но и их модификацию. Для этого используются HTTP-методы POST, PUT и DELETE. Для того, чтобы реализовать это в рамках сервиса, необходимо пометить нашу операцию в рамках публичного контракта атрибутом WebInvoke, вместо WebGet. При этом потребуется указать какой метод используется (свойство Method) и шаблон URI, на который отображается наша операция. В остальном реализация метода зависит от семантики операции. Например, мы можем что-то изменить в нашем источнике.

Не смотря на большое количество текста, получившегося при написании поста, реализация REST-сервиса на базе WCF вовсе не сложное занятие. Заканчивая, хочется подвести небольшой итог. Если у нас есть некая объектая модель данных (будь то концептуальная модель Entity Framework или модель LINQ to SQL, либо что-то еще), то несомненно реализовать REST-сервис будет проще на базе ADO.NET Data Services. WCF Syndication имеет смысл использовать в тех сценариях, где данные не очень удобно представлять в виде объектных структур. Например, если мы хотим предоставить доступ к каким-то файлам в рамках файловой системы, удобнее будет построить REST-сервис на базе WCF. Т.о. используя WCF мы можем реализовывать более глубокие сценарии, нежели просто предоставление доступа к модели данных через Web.