Создание сервиса WCF REST с поддержкой JSONP
Мне всегда импонировали фреймворки и языки, которые делались для того, чтобы сделать “простые вещи простыми, а сложные вещи возможными”. Надеясь именно на подобный расклад вещей, я решил посмотреть на то, как нынче делаются REST сервисы в WCF без помощи каких-либо библиотек (OpenRasta, MindTouch DReAM) или шаблонов вроде WCF REST Starter Kit. В этом посте – мои заметки насчет того, как все вышло.
Мотивация
Почему REST? Все очень просто – мне нынче импонируют идеи сайтов, которые находятся полностью на клиенте и работают с серверами именно через REST а не через WS-* или какие-то кастомные RPC-байндинги. REST – это тривиальная парадигма и, казалось бы, любой более менее продвинутый фреймворк должен позволять быстро и эффективно создавать REST-сервисы.
Поэтому я решил испробовать простенький сценарий – взять мой диалоговый фреймворк написанный на WebSharper и перетащить все данные на сервер, осуществив взаимодействие через REST.
Сущности
Первое что я обычно делаю в проекте который хранит данные – это конечно Install-Package norm дабы добавить драйвер NoRM для MongoDB. Коллега Суворов конечно рекоммендует якобы официальный драйвер от 10gen, но у меня итак все работает, в т.ч. Linq, поэтому зачем напрягаться?
Как вы помните, NoRM немного замусоривает наш объект, но он все еще остается “почти POCO”. WCF же в долгу не остается, и “домусоривает” объект еще больше, прописывая свои аттрибуты. В результате получаем классы подобные этому:
[DataContract]
public class ConversationItem
{
public ConversationItem()
{
Id = ObjectId.NewObjectId();
}
[DataMember, MongoIdentifier] public string Id { get; set; }
[DataMember] public string PartyId { get; set; }
[DataMember] public string Speech { get; set; }
[DataMember] public List<string> EnableList { get; set; }
[DataMember] public List<string> DisableList { get; set; }
}
Начинаем писать сервис
Первое что нужно сделать – удалить к черту сгенерировнный интерфейс – сервис проживет и без него, а [ServiceContract] можно навесить прямо на класс сервиса. Далее, можно создавать методы, но на них тоже нужно навесить аттрибуты, в частности:
OperationContractдля того чтобы пометить что это часть сервиса.WebGetдабы прописать шаблон вызова а также форматы запроса и ответа.JSONPBehavior, но он из коробки не поставляется, и мы о нем поговорим попозже.
Вот пример декорированного метода:
[OperationContract]
[WebGet(UriTemplate = "/c/{id}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
[JSONPBehavior(callback = "callback")]
public Conversation GetInitialConversationItems(string id)
{
...
}
Итак, “из коробки” мы получаем достаточно простой метод, но не хватает двух вещей – обработки ошибок и поддержки JSONP, без которой вы ничего кроссдоменно не вызовете.
Обработка ошибок
Для того чтобы возвращать всякие статус коды вроде Not Found, нужно перехватить выходящий ответ из WebOperationContext и прописать в него информацию о том, что собственно пошло не так. Например:
using (var db = GetDB())
{
var conversation = db.Query<Conversation>().FirstOrDefault();
if (conversation == null)
{
var resp = WebOperationContext.Current.OutgoingResponse;
resp.StatusCode = HttpStatusCode.NotFound;
resp.StatusDescription = "Could not find conversation with id='{0}'.".ƒ(id);
return null;
}
else
{
return conversation;
}
}
JSONP
Феноменально, но факт – WCF не поставляется с поддержкой JSONP. К счастью, Microsoft дает такую поддержку в примерах, и то как она выглядит является хорошей демонстрацией того, как гибок WCF в плане расширения.
Всего для поддержки JSONP нужно добавить 5 классов. Детально я описывать их не буду, опишу только вкратце.
- Во-первых, нужна реализация аттрибута
JSONPBehaviorкоторый я уже описывал. Фактически, этот аттрибут навешивает на операцию объект типаIParameterInspector, который перед вызовом прописывает в свойства исходящего сообщение свойство типаIMessagePropertyдля JSON. - Класс
JSONMessageProperty– это всего лишь обертка для лишнего кусочка метаданных который поставляются поведением. Применим этот довесок только для callback-параметра. Для тех кто забыл, callback-параметр это то с помошью чего данныеdataвозвражаются через запросhttp://somewhere.com/x?callback=yкакy(data), прописываются в<script>и исполняются. - Далее формируется фабрика
JSONPEncoderFactory, которая производит энкодеры типаJSONPEncoder. Сам энкодер, казалось бы, тривиален – все, что он должен сделать так это обернуть вызов в название callback’а и вернуть его. Но поскольку его методWriteMessage()перегружен, его приходится вызывать в нескольких местах.
Для того чтобы получить всю поддержку JSONP полностью, нужно скачать набор примеров по WCF c MSDN.
Web.config
Несмотря на то, что все, в принципе можно сделать в коде, на практике приходится достаточно сильно шаманить с Web.config ом дабы все заработало. В частности, нужно прописать новый binding:
<bindings>
<customBinding>
<binding name="jsonpBinding">
<jsonpMessageEncoding/>
<httpTransport manualAddressing="true"/>
</binding>
</customBinding>
</bindings>
А также добавить расширение для кодировки JSONP:
<extensions>
<bindingElementExtensions>
<add name="jsonpMessageEncoding"
type="ConversationServer.JsonpSupport.JsonpBindingExtension, ConversationServer"/>
</bindingElementExtensions>
</extensions>
Удаление .svc
Для REST-сервисов окончание .svc на конце сервиса как-то нелепо. К счастью его очень просто удалить. Для этого, мы можем создать свой собственный модуль который игнорирует это окончание:
public class RestModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += (sender, args) =>
{
var ctx = HttpContext.Current;
var path = ctx.Request.AppRelativeCurrentExecutionFilePath;
int i = path.IndexOf('/', 2);
if (i > 0)
{
var svc = path.Substring(0, i) + ".svc";
var rest = path.Substring(i, path.Length - i);
var qs = ctx.Request.QueryString.ToString();
ctx.RewritePath(svc, rest, qs, false);
}
};
}
public void Dispose()
{
}
}
А далее просто прописываем его в секцию system.web в Web.config:
<system.web>
<compilation debug="true" targetFramework="4.0" />
<customErrors mode="Off"/>
<httpModules>
<add name="NoMoreSVC" type="ConversationServer.RestModule, ConversationServer"/>
</httpModules>
</system.web>
Вот собственно и все.
Заключение
Мой небольшой эксперимент по использованию WCF REST показал что, как и во многих других случаях, приходится даже для простенького сервиса городить громозкие структуры. К счастью, сделав это один раз, можно потом копировать реализацию в различные проекты.


Как насчет нового WCF Web API?
http://wcf.codeplex.com
не пробовали?
Vladimir
26 Март 2011 в 10:43
Вот недавно скачал; буду разбираться.
Dmitri
26 Март 2011 в 22:23
Феноменально, но факт – WCF не поставляется с поддержкой JSONP
3 версия не поставляется, а вот в 4 все уже из коробки работает.
Алексей Калдузов
28 Март 2011 в 8:13
Точно! Спасибо за подсказку. Вот что бывает если импульсивно гуглить и сразу писать код :)
Dmitri
28 Март 2011 в 11:33
А как встроить поддержку REST в ASP.NET приложение?
Интересует проблема в плане того чтобы сервис не был доступен не прошедшим автроизацию и обращение к методам сервиса прошедшим авторизацию было бы прозрачным
Вадим
23 Февраль 2012 в 22:24
Добрый день, Дмитрий.
А как опубликовать сервис с удаленным окончанием svc на IIS? у меня не получилось
Александр
16 Июль 2012 в 11:43