Исходный код можно скачать по ссылке.
Продукты и технологии:
C++ REST SDK, OAuth
В статье рассматриваются:
- предыдущий пример Win32-приложения;
- варианты интеграции с Windows Runtime;
- использование класса WebAuthenticationBroker;
- создание цепочки асинхронных веб-запросов.
В предыдущей статье (msdn.microsoft.com/magazine/dn342869) я познакомил вас с C++ REST SDK и тем, как его можно использовать в Win32/MFC-приложениях. В этой статье мы обсудим, как C++ REST SDK можно интегрировать в приложения Windows Store. Одной из моих исходных целей в применении C++ REST SDK и класса аутентификации на основе OAuth было максимально полное использование стандартного C++ и лишь при необходимости взаимодействие со специфичными для платформы API. Кратко напомню суть предыдущей статьи.
- Код в классе аутентификации на основе OAuth использует только стандартные C++-типы - без специфичных для Windows типов.
- Код для выдачи веб-запросов к REST-сервису Dropbox использует типы из C++ REST SDK.
- Единственный специфичный для платформы код - это функция, которая запускает Internet Explorer, выполняет аутентификацию приложения и получает одобрение от портала Dropbox.
Я поставил те же цели в своем приложении Windows Store для поддержки аутентификации и загрузки файла в Dropbox. Я стремился написать максимально больше портируемого кода на C++ и взаимодействовать с Windows Runtime (WinRT) только при необходимости.
Проблемы с Win32-решением
Одним из крупных недостатков в предыдущем Win32-приложении была необходимость запуска внешнего приложения для выполнения процесса авторизации на основе OAuth. Это означало, что я должен был запускать Internet Explorer (можно было бы запускать и любой другой браузер), входить в Dropbox по своим удостоверениям, а затем выполнять необходимый рабочий процесс. Он проиллюстрирован на рис. 1 и 2.
Рис. 1. Вход в Dropbox по моим удостоверениям до авторизации доступа приложения
Рис. 2. Успешная авторизация моего приложения на портале Dropbox
Как видите, запуск внешнего приложения и предложение пользователям пройти рабочий процесс через внешнее приложение уводит фокус ввода из окна моего приложения. Как разработчик я также лишен стандартного механизма, через который мое приложение могло бы уведомляться о завершении этого рабочего процесса. При концентрации на асинхронном программировании и использовании C++ REST SDK, предназначенного для поддержки программирования на основе асинхронных задач, вынужденный запуск внешнего приложения со всей очевидностью является для меня неприятной ситуацией. Я исследовал подходы с применением именованных каналов (named pipes), проецируемых в память файлов (memory-mapped files) и т. д., но все эти подходы требуют создания другого приложения для хостинга экземпляра элемента управления "веб-браузер" и последующей записи значения, свидетельствующего об успехе, обратно через именованный канал, общую память или проецируемый в память файл. В итоге я остановился на использовании браузера для выполнения этой задачи, так как не хотел писать другую программу, которая обертывала бы элемент управления "веб-браузер".
Одним из крупных недостатков в предыдущем Win32-приложении была необходимость запуска внешнего приложения для выполнения процесса авторизации на основе OAuth.
Интеграция с Windows Runtime
Начав проектировать свое приложение под Windows Runtime, я рассмотрел несколько вариантов. Я вкратце расскажу о них здесь, а потом мы подробно обсудим выбранный мной подход.
- Используем активацию протокола и даем системе запустить подходящий процесс для обработки этого протокола, вызвав функцию Windows::System::Launcher::LaunchUriAsync. Это означает, что для URI на основе HTTPS операционная система запустит браузер по умолчанию. Это аналогично запуску Internet Explorer из Win32-примера, но с "двойной комиссией": мое приложение Windows Store станет фоновым процессом, браузер по умолчанию запустится в полноэкранном режиме, и в самом худшем случае мое приложение будет приостановлено на время выполнения пользователем рабочего процесса. Совершенно не годится!
- Интегрируем в приложение элемент управления WebView. Использование XAML-элемента WebView позволяет встроить всю навигацию по рабочему процессу в контекст моего приложения. Теоретически, я также могу получать уведомления о завершении процесса, слушая событие window.external.notify, генерируемое элементом управления WebView. Однако на практике это событие генерируется, только если веб-страница генерирует событие уведомления. В моем случае страница Dropbox, где выполняется процесс авторизации, такое событие не генерирует. Неприятная ситуация!
- Используем WebAuthenticationBroker в моем приложении. Продолжая копаться в Windows Runtime, я случайно наткнулся на класс WebAuthenticationBroker. Судя по всему, он мог бы помочь мне в выполнении процесса авторизации, и, действительно, я сумел создать всю необходимую функциональность на его основе. Прежде чем перейти к рассмотрению кода, позвольте мне пояснить некоторые детали, касающиеся WebAuthenticationBroker.
WebAuthenticationBroker
В мире подключенных приложений, чтобы получить согласие и одобрение пользователя, важно запрашивать его удостоверения через безопасный и доверяемый механизм. Никто не хочет быть разработчиком, чьи приложения допускают утечку удостоверений пользователя или оказываются уязвимыми к скрытым атакам с целью похищения информации о пользователе. Windows Runtime включает ряд API и необходимые технологии, позволяющие разработчику безопасно передавать удостоверения пользователя. WebAuthenticationBroker - одно из таких средств, которое дает возможность приложениям Windows Store использовать протоколы аутентификации и авторизации через Интернет, такие как OAuth и OpenID. Как же это работает в моем приложении-примере для Dropbox?
- Я выдаю начальный асинхронный запрос к Dropbox, который возвращает маркер и секрет для моего приложения. Этот начальный запрос передается через функцию oAuthLoginAsync.
- Как только функция oAuthLoginAsync возвращает управление, я конструирую в продолжении последовательности URI, где должен начаться процесс авторизации. В своем примере я определил начальный URI как строковую константу:
- const std::wstring DropBoxAuthorizeURI =
- L"https://www.dropbox.com/1/oauth/authorize?oauth_token=";
- Затем я формирую URI HTTP-запроса, дописывая маркер, возвращенный Dropbox.
- В качестве дополнительного шага я конструирую параметр с URI обратного вызова, обращаясь к функции WebAuthenticationBroker::GetCurrentApplicationCallbackUri. Заметьте, что я не использовал URI обратного вызова в своем настольном приложении, так как этот параметр не обязателен и я полагался на Internet Explorer в выполнении задачи авторизации.
- Теперь строка запроса готова, и я могу выдать запрос. Вместо использования класса http_client или интерфейса IHttpWebRequest2 из C++ REST SDK для вызовов веб-сервиса я вызываю функцию WebAuthenticationBroker::AuthenticateAsync.
- Функция WebAuthenticationBroker::AuthenticateAsync принимает два параметра: перечисление WebAuthenticationOptions и URI. Перегруженный экземпляр той же функции принимает перечисление WebAuthenticationOptions и два URI, по одному из которых начинается процесс аутентификации, а по другому - заканчивается.
- Я использую первую версию функции AuthenticateAsync и передаю значение None для перечисления WebAuthenticationOptions, а также URI, сформированный для моего веб-запроса.
- WebAuthenticationBroker размещается между моим приложением и системой. В точке, где я вызываю AuthenticateAsync, он создает системный модальный диалог, который является модальным для моего приложения.
- Брокер подключает окно веб-хоста к созданному им модальному диалоговому окну.
- Затем брокер выбирает выделенный процесс контейнера приложений, отделенный от контейнера, в котором выполняется мое приложение. Это также приводит к очистке любых сохраненных данных в моем приложении.
- Далее брокер начинает процесс аутентификации в этом только что выбранном контейнере приложения и переходит к URI, указанному функцией AuthenticateAsync.
- Когда пользователи взаимодействуют с веб-страницами, брокер проверяет каждый URL для указанного URI обратного вызова.
- Как только обнаруживается совпадение, веб-хост прекращает навигацию и посылает брокеру сигнал. Брокер убирает диалоговое окно, очищает любые сохраненные файлы cookie, созданные веб-хостом, из контейнера приложения и возвращает данные протокола обратно приложению.
В мире подключенных приложений, чтобы получить согласие и одобрение пользователя, важно запрашивать его удостоверения через безопасный и доверяемый механизм.
Рис. 3 иллюстрирует модальный диалог WebAuthenticationBroker в приложении-примере Dropbox после того, как веб-хост перешел на начальный URI. Поскольку Dropbox ожидает входа пользователей до появления страницы авторизации, веб-хост перенаправляет процесс навигации на страницу входа в Dropbox.
Рис. 3. Страница входа в Dropbox, показываемая в модальном диалоге
Как только пользователь вошел в Dropbox, веб-хост переходит к URI авторизации. Это отражено на рис. 4. Из рис. 3 и 4 понятно, что диалог размещается поверх UI моего приложения. UI также остается согласованным безотносительно приложения-источника, вызывающего метод WebAuthenticationBroker::AuthenticateAsync. Поскольку вся пользовательская среда сохраняет согласованность, пользователи могут предоставлять удостоверения, не беспокоясь о приложениях, обрабатывающих эту информацию, и о ее случайной утечке.
Рис. 4. Dropbox запрашивает согласие пользователя на авторизацию приложения
Одна важная вещь, о которой я не упомянул, - необходимость вызова функции WebAuthenticationBroker::AuthenticateAsync из UI-потока. Все веб-запросы в C++ REST SDK выдаются в фоновом потоке, а вывести UI из фонового потока нельзя. Поэтому я использую системный диспетчер и вызываю его функцию-член RunAsync для отображения модального UI (рис. 5).
Рис. 5. Использование системного диспетчера для отображения модального UI
- auto action = m_dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this]()
- {
- auto beginUri = ref new Uri(ref new String(m_authurl.c_str()));
- task<WebAuthenticationResult^> authTask(WebAuthenticationBroker::
- AuthenticateAsync(WebAuthenticationOptions::None, beginUri));
- authTask.then([this](WebAuthenticationResult^ result)
- {
- String^ statusString;
- switch(result->ResponseStatus)
- {
- case WebAuthenticationStatus::Success:
- {
- auto actionEnable = m_dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this]()
- {
- UploadFileBtn->IsEnabled = true;
- }));
- }
- }
- });
- }));
Как только пользователь вошел в Dropbox, веб-хост переходит к URI авторизации.
По окончании процесса авторизации я снова запускаю диспетчер, чтобы сделать доступной кнопку Upload File в основном UI. Эта кнопка остается недоступной, пока пользователи не аутентифицировали и не авторизовали мое приложение для доступа к Dropbox.
Создание цепочки асинхронных веб-запросов
Теперь легко свести все воедино. Во всех функциях, не взаимодействующих с Windows Runtime, я повторно использовал код из моего настольного приложения. Крупных изменений в коде нет, кроме одного: в функции UploadFileToDropboxAsync вместо C++ iostream используется WinRT-объект StorageFile.
При написании приложений Windows Store приходится учитывать некоторые ограничения, с которыми вы должны смириться. Одно из них - необходимость использования WinRT-объектов StorageFile вместо C++-потоков (streams) для чтения и записи данных в файлы. При разработке приложения Windows Store с применением C++ REST SDK все операции, связанные с файлами, ожидают передачи объекта StorageFile, а не C++-объекта потока данных. Внеся это небольшое изменение, я смог повторно использовать весь свой стандартный C++-код, поддерживающий код OAuth-авторизации и Dropbox, в приложении-примере для Windows Store.
Вот как выглядит соответствующий псевдокод (индивидуальные функции мы обсудим после этого псевдокода):
При щелчке кнопки SignIn Вызов функции oAuthLoginAsync Затем вызов WebAuthenticationBroker::AuthenticateAsync Затем делаем доступной кнопку Upload File в UI При щелчке кнопки Upload File Вызов функции Windows::Storage::Pickers::FileOpenPicker:: PickSingleFileAsync Затем вызов функции oAuthAcquireTokenAsync Затем вызов функции UploadFileToDropboxAsync
В обработчике событий кнопки SignInBtnClicked, показанном на рис. 6, я сначала выполняю простую проверку параметров, чтобы удостовериться, что в параметрах ConsumerKey и ConsumerSecret не передаются пустые строки, отправляемые потом в Dropbox для аутентификации. Затем я получаю экземпляр объекта Dispatcher, сопоставленного с текущим потоком CoreWindow, и сохраняю его как переменную-член класса MainPage. Dispatcher отвечает за обработку оконных сообщений и диспетчеризацию событий для приложения. Далее я создаю экземпляр класса OnlineIdAuthenticator. Этот класс содержит вспомогательные функции, которые позволяют мне выводить модальное диалоговое окно приложения и выполнять защищенный рабочий процесс авторизации. Это избавляет от необходимости запуска экземпляра браузера и перевода фокуса ввода с приложения в браузер.
Рис. 6. Функция SignInBtnClicked
- void MainPage::SignInBtnClicked(Platform::Object^ sender,
- RoutedEventArgs^ e)
- {
- if ((ConsumerKey->Text == nullptr) //
- (ConsumerSecret->Text == nullptr))
- {
- using namespace Windows::UI::Popups;
- auto msgDlg = ref new MessageDialog(
- "Please check the input for the Consumer Key and/or Consumer Secret tokens");
- msgDlg->ShowAsync();
- }
- m_dispatcher =
- Windows::UI::Core::CoreWindow::GetForCurrentThread()->Dispatcher;
- m_creds = std::make_shared<AppCredentials>();
- m_authenticator = ref new OnlineIdAuthenticator();
- consumerKey = ConsumerKey->Text->Data();
- consumerSecret = ConsumerSecret->Text->Data();
- ConsumerKey->Text = nullptr;
- ConsumerSecret->Text = nullptr;
- OAuthLoginAsync(m_creds).then([this]
- {
- m_authurl = DropBoxAuthorizeURI;
- m_authurl +=
- utility::conversions::to_string_t(this->m_creds->Token());
- m_authurl += L"&oauth_callback=";
- m_authurl += WebAuthenticationBroker::
- GetCurrentApplicationCallbackUri()->AbsoluteUri->Data();
- auto action = m_dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this]()
- {
- auto beginUri = ref new Uri(ref new String(m_authurl.c_str()));
- task<WebAuthenticationResult^>authTask(
- WebAuthenticationBroker::AuthenticateAsync(
- WebAuthenticationOptions::None, beginUri));
- authTask.then([this](WebAuthenticationResult^ result)
- {
- String^ statusString;
- switch(result->ResponseStatus)
- {
- case WebAuthenticationStatus::Success:
- {
- auto actionEnable = m_dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this]()
- {
- UploadFileBtn->IsEnabled = true;
- }));
- }
- }
- });
- }));
- }
Затем я вызываю функцию OAuthLoginAsync, которая выполняет операцию входа в Dropbox. Как только эта асинхронная функция возвращает управление, я использую функцию RunAsync объекта Dispatcher для маршалинга вызова обратно в UI-поток из фонового потока асинхронной задачи. Функция RunAsync принимает два параметра: значение приоритета и экземпляр DispatchedHandler. Я задаю приоритет как "Normal" и передаю функцию лямбды экземпляру DispatchedHandler. В теле лямбды я вызываю статическую функцию AuthenticateAsync класса WebAuthenticationBroker, которая потом отображает модальный диалог приложения и помогает выполнить защищенную аутентификацию.
При написании приложений Windows Store приходится учитывать некоторые ограничения, с которыми вы должны смириться.
По окончании рабочего процесса диалог удаляется, и функция возвращает либо код успешного завершения, либо обнаруженные ошибки. В моем случае я просто обрабатываю возвращаемый тип WebAuthenticationStatus::Success и снова использую объект диспетчера, чтобы сделать доступной кнопку UploadFile в UI. Поскольку все вызываемые мной функции являются асинхронными, мне нужно задействовать объект диспетчера для маршалинга вызовов в UI-поток, если я хочу обращаться к каким-либо UI-элементам.
Обработчик событий UploadFileBtnClicked показан на рис. 7. В самом обработчике кода не так уж много. Я вызываю функцию FileOpenPicker::PickSingleFileAsync, которая позволяет выбрать один текстовый файл через интерфейс выбора (picker interface). Затем вызываю функцию OAuthAcquireTokenAsync (рис. 8) и при успешном завершении обращаюсь к функции UploadFileToDropBoxAsync (рис. 9).
Рис. 7. Функция UploadFileBtnClicked
- void MainPage::UploadFileBtnClicked( Platform::Object^ sender,
- RoutedEventArgs^ e)
- {
- using namespace Windows::Storage::Pickers;
- using namespace Windows::Storage;
- auto picker = ref new FileOpenPicker();
- picker->SuggestedStartLocation = PickerLocationId::DocumentsLibrary;
- picker->FileTypeFilter->Append(".txt");
- task<StorageFile^> (picker->PickSingleFileAsync())
- .then([this](StorageFile^ selectedFile)
- {
- m_fileToUpload = selectedFile;
- OAuthAcquireTokenAsync(m_creds).then([this](){
- UploadFileToDropBoxAsync(m_creds);
- });
- });
- }
Рис. 8. Функция OAuthAcquireTokenAsync
- task<void> MainPage::OAuthAcquireTokenAsync(
- std::shared_ptr<AppCredentials>& creds)
- {
- uri url(DropBoxAccessTokenURI);
- std::shared_ptr<OAuth> oAuthObj = std::make_shared<OAuth>();
- auto signatureParams =
- oAuthObj->CreateOAuthSignedParameters(url.to_string(),
- L"GET",
- NULL,
- consumerKey,
- consumerSecret,
- creds->Token(),
- creds->TokenSecret()
- );
- std::wstring sb = oAuthObj->OAuthBuildSignedHeaders(url);
- http_client client(sb);
- // Выдаем запрос и асинхронно обрабатываем ответ
- return client.request(methods::GET)
- .then([&creds](http_response response)
- {
- if(response.status_code() != status_codes::OK)
- {
- auto stream = response.body();
- container_buffer<std::string> inStringBuffer;
- return stream.read_to_end(inStringBuffer)
- .then([inStringBuffer](pplx::task<size_t> previousTask)
- {
- UNREFERENCED_PARAMETER(previousTask);
- const std::string &text = inStringBuffer.collection();
- // Преобразуем текст ответа в широкосимвольную строку
- std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,
- wchar_t> utf16conv;
- std::wostringstream ss;
- ss << utf16conv.from_bytes(text.c_str()) << std::endl;
- OutputDebugString(ss.str().data());
- // Обработка ошибок
- return pplx::task_from_result();
- });
- }
- // Здесь выполняем операции, читая из потока ответа
- istream bodyStream = response.body();
- container_buffer<std::string> inStringBuffer;
- return bodyStream.read_to_end(inStringBuffer)
- .then([inStringBuffer, &creds](pplx::task<size_t> previousTask)
- {
- UNREFERENCED_PARAMETER(previousTask);
- const std::string &text = inStringBuffer.collection();
- // Преобразуем текст ответа в широкосимвольную строку
- std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>,
- wchar_t> utf16conv;
- std::wostringstream ss;
- std::vector<std::wstring> parts;
- ss << utf16conv.from_bytes(text.c_str()) << std::endl;
- Split(ss.str(), parts, '&', false);
- unsigned pos = parts[1].find('=');
- std::wstring token = parts[1].substr(pos + 1, 16);
- pos = parts[0].find('=');
- std::wstring tokenSecret = parts[0].substr(pos + 1);
- creds->SetToken(token);
- creds->SetTokenSecret(tokenSecret);
- });
- });
- }
Рис. 9. Функция UploadFileToDropBoxAsync
- task<void> MainPage::UploadFileToDropBoxAsync(
- std::shared_ptr<AppCredentials>& creds)
- {
- using concurrency::streams::file_stream;
- using concurrency::streams::basic_istream;
- uri url(DropBoxFileUploadURI);
- std::shared_ptr<oAuth> oAuthObj = std::make_shared<oAuth>();
- auto signatureParams =
- oAuthObj->CreateOAuthSignedParameters(url.to_string(),
- L"PUT",
- NULL,
- consumerKey,
- consumerSecret,
- creds->Token(),
- creds->TokenSecret()
- );
- std::wstring sb = oAuthObj->OAuthBuildSignedHeaders(url);
- return file_stream<unsigned char>::open_istream(this->m_fileToUpload)
- .then([this, sb, url](pplx::task<basic_istream<unsigned char>> previousTask)
- {
- try
- {
- auto fileStream = previousTask.get();
- // Получаем длину контента,
- // присвоенного свойству Content-Length
- fileStream.seek(0, std::ios::end);
- auto length = static_cast<size_t>(fileStream.tell());
- fileStream.seek(0, 0);
- // Выдаем HTTP-запрос с файловым потоком в качестве тела
- http_request req;
- http_client client(sb);
- req.set_body(fileStream, length);
- req.set_method(methods::PUT);
- return client.request(req)
- .then([this, fileStream](pplx::task<http_response> previousTask)
- {
- fileStream.close();
- std::wostringstream ss;
- try
- {
- auto response = previousTask.get();
- auto body = response.body();
- // Протоколируем код успешного ответа
- ss << L"Server returned status code "
- << response.status_code() << L"."
- << std::endl;
- OutputDebugString(ss.str().data());
- if (response.status_code() == web::http::status_codes::OK)
- {
- auto action = m_dispatcher->RunAsync(
- Windows::UI::Core::CoreDispatcherPriority::Normal,
- ref new Windows::UI::Core::DispatchedHandler([this]()
- {
- using namespace Windows::UI::Popups;
- auto msgDlg = ref new MessageDialog(
- "File uploaded successfully to Dropbox");
- msgDlg->ShowAsync();
- }));
- }
- }
- catch (const http_exception& e)
- {
- ss << e.what() << std::endl;
- OutputDebugString(ss.str().data());
- }
- });
- }
- catch (const std::system_error& e)
- {
- // Здесь протоколируем любые ошибки
- // и возвращаем пустую задачу
- std::wostringstream ss;
- ss << e.what() << std::endl;
- OutputDebugString(ss.str().data());
- return pplx::task_from_result();
- }
- });
- }
Функция OAuthAcquireTokenAsync получает маркер, сопоставленный с учетной записью Dropbox. Сначала я формирую необходимую строку доступа (access string) и заголовки HTTP-запроса и вызываю сервис Dropbox для проверки удостоверений. Этот HTTP-запрос имеет тип GET, а ответ возвращается как поток символов. Я разбираю этот поток для получения самого маркера и секрета, которые потом сохраняются в экземпляре класса AppCredentials.
Успешно получив маркер и его секрет от Dropbox, я использую их для загрузки файла в Dropbox. Как и в случае любой конечной точки веб-доступа к Dropbox, сначала формируется строка параметров и HTTP-заголовки. Затем вызывается конечная точка сервиса Dropbox, сопоставленная с загрузкой файлов. Этот HTTP-запрос имеет тип PUT, поскольку я пытаюсь поместить контент в сервис. Перед этим мне также нужно сообщить Dropbox о размере контента. Это указывается установкой значения свойства content_length в методе HTTP_request::set_body равным размеру загружаемого файла. После успешного возврата PUT-метода с помощью объекта диспетчера я вывожу пользователю сообщение об успешном завершении операции.
На очереди Linux
Интеграция C++ REST SDK в приложения Windows 8 (как Windows Store, так и настольные) проста и прямолинейна. Добавьте преимущества написания кода, который может быть общим между двумя платформами, применение идиом программирования на современном C++ и тот факт, что данный код является портируемым между приложениями как Windows, так и других ОС, - и приз ваш. Вы можете больше не беспокоиться о специфичных для платформ тонкостях, относящихся к сетевым API, и вместо этого уделять больше времени продумыванию функциональности, которую должно поддерживать ваше приложение. В этом простом примере я задействовал C++ REST SDK для аутентификации пользователя в Dropbox и последующей загрузки файла в облако Dropbox. Подробнее о Dropbox REST API см. документацию по ссылке bit.ly/10OdTD0. В следующей статье я покажу, как выполнять те же задачи из Linux-клиента.
Сридхар Подури (Sridhar Poduri) - менеджер программ в группе Windows в Microsoft. Страстный поклонник C++ и автор книги "Modern C++ and Windows Store Apps" (Sridhar Poduri, 2013), регулярно пишет о C++ и Windows Runtime в своем блоге sridharpoduri.com.
Выражаю благодарность за рецензирование статьи экспертам Microsoft Никласу Густаффсону (Niklas Gustaffson), Сана Митани (Sana Mithani) и Огги Шобахичу (Oggy Sobajic).