С выходом Windows Phone 8.1 появилась новая возможность в разработке приложений магазина Windows / Windows phone с общей кодовой базой. Это так называемые универсальные приложения, базирующиеся на более общем API и возможности повторного использования разметки XAML в Visual Studio 2013 сразу из коробки.
Если приложение магазина Windows использует WCF для работы с SOAP сервисами, то попытка портирования на Windows phone может завершиться неудачей. Как оказалось, пространство имен System.ServiceModel отныне недоступно. Соответственно требуется замена, отвечающая следующим требованиям:
- Простота использования, аналогично старому Add Service Reference, где на выходе получается сгенерированный код строго типизированного клиента сервиса;
- Расширяемость, как показала практика может потребоваться поддержка различных схем аутентификации.
SOAP запрос представляет собой особым образом сформированный XML документ. Все что необходимо сделать это сериализовать данные запроса в XML, поместить их в элемент Body и отправить в теле HTTP POST запроса. Структура ответа аналогична, результат получается из элемента Body.
На основе HttpClient реализуем базовый класс для формирования запроса. В результате получаем функцию следующего вида:
public async Task<TResponse> CallAsync<TRequest, TResponse>(string action, TRequest request)
{
IHttpContent httpContent = GetHttpContent(action, request);
var response = await Client.PostAsync(EndpointAddress, httpContent);
var responseContent = await response.Content.ReadAsStringAsync();
return GetResponse<TResponse>(responseContent);
}
Остается только подставить соответствующие классы TRequest и TResponse которые можно получить на основе описания сервиса. SOAP сервисы описываются при помощи Web Services Description Language ( WSDL), языка, описывающего API сервиса в виде операций и типов данных.
К получению информации о сервисе можно подойти с разных сторон:
- Работа с сырым WSDL. Можно, но для поставленной задачи слишком трудоемко, пойдем лучше по пути наименьшего сопротивления;
- Использовать класс System.ServiceModel.Description.MetadataExchangeClient, это самый разумный вариант, однако реализация этого класса не позволяет работать с любым WSDL. Если в документе содержится элемент import, то MetadataExchangeClient вываливается с ошибкой;
- Использовать консольную утилиту SvcUtilвходящую в состав .Net Framework SDK. На выходе мы получим тот же исходный код что и при использовании Add Service Reference из Visual Studio, который и возьмем за основу.
Исходный код полученный при помощи SvcUtil не может напрямую использоваться для приложений магазина Windows Phone по причине наличия следующих не поддерживаемых конструкций:
- Клиент сервиса на основе System.ServiceModel.ClientBase<T>. Использовать нельзя, но можно взять за основу интерфейс сервиса.
- Атрибутов System.SerializableAttribute и System.ComponentModel.DesignerCategoryAttribute. Эти аттрибуты не поддерживаются, удаляем.
- Свойств public System.Xml.XmlElement[] Any { get; set; }. Меняем тип на System.Xml.Linq.XElement[].
Преобразование кода производится при помощи Reflection + CodeDom. В итоге получаем реализацию классов TRequest и TRespone совместимых с платформой. Осталось только сгенерировать клиент сервиса реализующий соответствующий интерфейс. Всю необходимая для этого информация так же получается из исходного файла.
В результате, путем не хитрых манипуляций можно сгенерировать клиент для любого сервиса. В дополнении к этому API сервиса совпадает с тем что было получено при использовании WCF. Неизменность API позволяет подменить клиент сервиса без редактирования существующего кода.
Расширяемость этого решения основана на возможности использования конструктора класса HttpClient, принимающего параметр IHttpFilter, за счет которого достаточно легко реализовать шаблон проектирования Декоратор. Таким образом, можно не затрагивая существующее API работать с HTTP заголовками, например, чтобы реализовать Digest аутентификацию, так и модифицировать SOAP запрос перед отправкой чтобы добавить требуемые параметры в элемент Header.
private static HttpClient GetClient(PasswordCredential cred, TimeSpan? timeDiff)
{
var baseFilter = new HttpBaseProtocolFilter
{
AllowUI = false,
};
if (cred == null)
return new HttpClient(baseFilter);
var httpDigestHttpFilter = new HttpDigestHttpFilter(baseFilter)
{
UserName = cred.UserName,
Password = cred.Password
};
var soapDigestHttpFilter = new SoapDigestHttpFilter(httpDigestHttpFilter)
{
UserName = cred.UserName,
Password = cred.Password,
TimeDiff = timeDiff
};
return new HttpClient(soapDigestHttpFilter);
}
Приложения магазина Windows менее обделены функционалом чем Windows Phone и сохранили некоторое подмножество WCF. Однако и тут можно столкнуться с проблемой, не позволяющей воспользоваться существующим функционалом. Проблема эта заключается в отправке с каждым запросом заголовка Expect: 100-Continue. Некоторые сервисы категорично отвечают на это ошибкой 417 Expectation Failed. В полной версии .Net Framework это решается сбросом флага Expect100Continue:
System.Net.ServicePointManager.Expect100Continue = false;
В .Net для приложений магазина Windows это свойство отсутствует. При использовании HttpClient это ограничение можно обойти и использовать полученную реализацию в том числе и для приложений магазина Windows.
Генерация исходного кода производится получившейся консольной утилитой в следующем формате:
SoapClientGenerator.exe <metadataDocumentPath> <file> <namespace> [/svcutil:<svcutilPath>]
Где:
- <metadataDocumentPath> — путь к WSDL
- <file> — путь к выходному файлу
- <namespace> — пространство имен выходного файла
- /svcutil:<svcutilPath> — необязательный параметр, путь к svcutil, по умолчанию C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\SvcUtil.exe
Исходный код доступен на GitHub.