СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Близкая к идеалу адаптация ВКонтакте API для платформы .NET в черновиках Tutorial

Здравствуйте, дорогие Хабравчане!


Мало для кого является секретом, что за последние несколько лет одноимённая социальная сеть успела основательно войти людям в привычку и вырасти до масштабов сервиса континентального уровня.


За это время пространство ВКонтакте активно осваивали все, кто увидел там какой-либо потенциал, и сегодня в нём существует множество проектов, нацеленных на аудиторию с различными предпочтениями.


Когда работа с информацией приобретает решающее значение, становится очевидно, что в стремлении к лучшим результатам люди будут нуждаться в новых и не всегда стандартных решениях обработки данных.


Меня зовут Илья Терещук, на сегодняшний день я живу ведением проекта в социальной сети и занимаюсь программированием. Создавая своё первое приложение для работы с API, я столкнулся с немалым количеством нюансов, которые, по всей видимости, дают изрядно поломать голову любому, кто берётся за подобное впервые.


В разработке фундаментального слоя взаимодействия для конкретного интерфейса главной задачей стоит исключить любые "подводные камни" в его функционировании и обеспечить хорошую встраиваемость этого слоя как компонента для любого решения. К слову, методы реализации такой парадигмы называются паттернами, а умение их применять является прерогативой грамотных программистов. Следовательно, данная статья и будет показательным примером того, как внимание к мелочам создаёт качественные решения.


Ознакомление с ВКонтакте API


Как обычно, мы начинаем знакомство с интерфейсом с открытия главной страницы документации API.







Здесь нам дают понять, что для того, чтобы работать с данными в ВК, нужно зарегистрировать приложение.







После этого устанавливаем состояние на "Включено и видно всем" и сразу копируем ID приложения в код.







Работа с авторизацией ВКонтакте API


В первой главе документации мы видим, что приложение сможет работать только в том случае, если пользователь социальной сети произведёт авторизацию в его контексте и даст добро на права доступа.







Это можно выполнить только посредством вызова браузера (данный момент обещает трудности).







Ограничение на частоту запросов ВКонтакте API


Любой интерфейс, который рассчитан на массовое использование, обязан обладать элементарным механизмом ограничения потока данных для того, чтобы ему не могли дать больше запросов, чем он способен выполнить.
Вызов большего количества запросов на момент времени, чем это позволяет ограничение, приведёт к тому, что вместо данных клиент будет получать сообщения об ошибках. В связи с этим на самом первичном уровне нужно обеспечить логику, согласно которой все обращения к API выстраиваются в очередь и растягиваются по времени. Учитывая то, что в серьёзных приложениях обращения выполняются параллельно и асинхронно, реализация такой архитектуры вряд ли будет тривиальной задачей.







Ограничение на объёмы возвращаемых данных ВКонтакте API


Для абсолютной стабильности работы интерфейса ограничить частоту запросов мало: представьте, каково придётся серверу, если сеть из нескольких тысяч ботов одновременно пожелает получить весь список подписчиков сообщества MDK пользователей, находящихся в сети. Во избежание подобных случаев API устанавливает пороговый максимум для массивов данных. Это означает, что нам обязательно нужно будет реализовать функционал, который, опираясь на "очередь" из предыдущего абзаца, сможет назначать распределённые вызовы множеств однотипных методов, отслеживать общий прогресс их выполнения и возвращать результат в виде объединённых массивов.







Разработка логики авторизации для ВКонтакте API


После ознакомления с интерфейсом и выяснения проблемных моментов мы готовы приступить непосредственно к разработке. В первую очередь это будет механизм авторизации, так как при его отсутствии подавляющее большинство функционала API просто недоступно.


В самом начале нужно определить класс, который будет содержать установки для выполнения авторизации:


public class VKAPIAuthorizationSettings
{
    // > Разрешения, передаваемые в параметре "scope"
    public VKAPIAuthorizationPermissions ApplicationPermissions { get; set; }

    // > Идентификатор приложения ВКонтакте
    public String ApplicationIdentity { get; set; }

    // > Флаг на постоянное переспрашивание авторизации
    public Boolean RevocationEnabled { get; set; }

    // > Используемая версия API 
    public String APIVersion { get; set; }

    // > Создание строки запроса на основе параметров
    public Uri GetAuthorizationUri()
    {
        // + Инициализация строки запроса на авторизацию
        var authorizationUriBuilder = new UriBuilder("https://oauth.vk.com/authorize");
        var queryBuilder = HttpUtility.ParseQueryString(String.Empty);
        // ++ Присваивание неизменяющихся параметров
        queryBuilder["display"] = "popup";
        queryBuilder["response_type"] = "token";
        queryBuilder["redirect_uri"] = "https://oauth.vk.com/blank.html";
        // -- Присваивание неизменяющихся параметров
        // ++ Присваивание параметров, переданных в конфигурации
        queryBuilder["v"] = APIVersion;
        queryBuilder["client_id"] = ApplicationIdentity;
        queryBuilder["scope"] = ApplicationPermissions.ToString();
        if (RevocationEnabled) queryBuilder["revoke"] = "1";
        // -- Присваивание параметров, переданных в конфигурации
        authorizationUriBuilder.Query = queryBuilder.ToString();
        // - Инициализация строки запроса на авторизацию
        return authorizationUriBuilder.Uri;
    }
}

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


public class VKAPIAuthorizationPermissions
{
    // > Названия полей в точности копируют таковые в спецификации API
    public bool notify;
    public bool friends;
    public bool photos;
    public bool audio;
    public bool video;
    public bool docs;
    public bool notes;
    public bool pages;
    public bool status;
    public bool wall;
    public bool groups;
    public bool messages;
    public bool email;
    public bool notifications;
    public bool stats;
    public bool ads;
    public bool market;
    public bool offline = true; // > Для получения неистекаемого ключа (важно)
    public bool nohttps;

    // > Метод, преобразующий объект разрешений в строковый фрагмент для URI
    public override String ToString()
    {
        // >> Эта операция - одно из проявлений использования "рефлексии"
        // >> Внутри класса происходит чтение названий (!) его же полей
        var fieldsInformation = 
            typeof(VKAPIAuthorizationPermissions)
            .GetFields(BindingFlags.Public | BindingFlags.Instance);

        var includedPermissions = new List<String>();

        foreach (var fieldInfo in fieldsInformation)
        {
            // >>> Если сканируемое поле имеет значение true
            if ((bool)fieldInfo.GetValue(this))
            {
                // >>>> Добавляем название этого поля в список строк
                includedPermissions.Add(fieldInfo.Name);
            }
        }
        // >> По завершению операции возвращаем названия полей, разделённые запятой
        return String.Join(",", includedPermissions);
    }
}

Логика формирования запроса для авторизации готова, теперь можно заняться ей самой. Насколько нам уже известно, для этого требуется диалог на WPF со встроенным браузером из библиотеки Windows Forms:


Почему стоковый WebBrowser из WPF не подходит для этой задачи

Механизм авторизации ВКонтакте API устроен так, чтобы возвращать ссылку, параметры в которой отделяются от адреса не знаком вопроса, а решёткой. К своему удивлению я обнаружил, что WebBrowser из WPF не может распарсить эту конструкцию и вернуть данные, а благодаря поискам на StackOverflow и MSDN выяснилось, что такой баг действительно есть и единственным вариантом является подключить WebBrowser из библиотеки Windows Forms.


<Window x:Class="VKAPIUtilities.VKAPIAdapter.Authorization.VKAPIAuthorizationWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        xmlns:wfi="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
        ResizeMode="CanMinimize"
        Loaded="Window_Loaded"
        SizeToContent="WidthAndHeight"
        WindowState="Minimized"
        Icon="pack://application:,,,/VKAPIUtilities.VKAPIAdapter;component/Resources/Icons/vk.png">
    <!-- Пространства имён wf и wfi требуют включения в проект библиотек Windows Forms -->
    <Grid>
        <wfi:WindowsFormsHost>
            <wf:WebBrowser
                x:Name="webBrowser"
                ScrollBarsEnabled="False" />
        </wfi:WindowsFormsHost>
    </Grid>
</Window>

Перед тем, как рассмотреть код диалога, важно заметить, что мы ждём от него следующего результата:


public class VKAPIAuthorizationResult
{
    // > Идентификатор пользователя, от имени которого произошла авторизация
    public String UserIdentity { get; set; }
    // > Ключ, который возвращается при успешной авторизации
    public String AccessToken { get; set; }
}

Для начала проанализируем момент инициализации диалога. Как видите, его конструктор принимает установки, описанные в предыдущих абзацах, а когда экземпляр диалога создан, на браузер привязываются обработчики событий и запускается переход по ссылке авторизации, полученной из объекта установок:


public VKAPIAuthorizationWindow(VKAPIAuthorizationSettings authorizationSettings)
{
    _authorizationSettings = authorizationSettings;
}

private void Window_Loaded(object sender, RoutedEventArgs arguments)
{
    // > WebBrowser базируется на движке IE и "не понимает" последних стандартов JavaScript 
    // > Действием в строке ниже мы выключаем сообщения о каждом несоответствии в скриптах
    webBrowser.ScriptErrorsSuppressed = true;

    // > Вызывается, когда браузер начинает переходить по определенной ссылке
    webBrowser.Navigated += WebBrowser_Navigated;

    // > Вызывается, когда в окне браузера страница загрузилась целиком
    webBrowser.DocumentCompleted += WebBrowser_DocumentCompleted;

    // > Интерфейс готов, обработчики привязаны - можно начинать авторизацию
    webBrowser.Navigate(_authorizationSettings.GetAuthorizationUri());
}

В обработчике переходов и есть тот заветный код, который предохраняет нас от "выстрела себе в ногу":


private void WebBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs arguments)
{
    // > Пока страница не загрузилась, свернем окно
    WindowState = WindowState.Minimized; 
    // > Достанем из URI часть, которая идёт после адреса
    var uriQueryFragment = arguments.Url.Fragment;
    // > При авторизации VK возвращает ссылку типа https://oauth.vk.com/blank.html#access_token=...
    // > Это особенная ситуация, так как отделение параметров знаком # не является стандартом
    if (uriQueryFragment.StartsWith("#"))
    {
        // >> Для того, чтобы парсер смог обработать фрагмент запроса, требуется убрать этот символ
        uriQueryFragment = uriQueryFragment.Replace("#", String.Empty);
    }
    // > Соответственно, теперь можно её распарсить
    var queryParameters = HttpUtility.ParseQueryString(uriQueryFragment);
    // > Состояние интерфейса авторизации нужно отслеживать по параметрам в строке навигации
    // > В певую очередь проверим, не содержит ли строка параметра, который означает отмену
    var isCancelledByUser = !String.IsNullOrEmpty(queryParameters["error"]);
    if (isCancelledByUser)
    {
        // >> Если таковой присутствует, завершим диалог
        DialogResult = false;
    }
    else
    {
        // >> Если пользователь не отменял процесс, возможно, он как раз авторизовался
        var isAccessTokenObtained = !String.IsNullOrEmpty(queryParameters["access_token"]);
        var isUserIdentityObtained = !String.IsNullOrEmpty(queryParameters["user_id"]);
        if (isAccessTokenObtained && isUserIdentityObtained)
        {
            // >>> В таком случае запишем полученные параметры в переменную
            _authorizationResult = new VKAPIAuthorizationResult
            {
                AccessToken = queryParameters["access_token"],
                UserIdentity = queryParameters["user_id"]
            };
            // >>> Теперь с завершением диалога можно вернуть данные
            DialogResult = true;
        }
        else
        {
            // >> Пока пользователь ничего не отменял и еще не авторизовался
        }
    }
}

Не лишним будет раскрыть момент реализации кастомного диалога:


public new VKAPIAuthorizationResult ShowDialog()
{
    InitializeComponent();
    // > Программа останавливается на этом месте, пока не присвоен DialogResult
    base.ShowDialog();
    // > Когда DialogResult будет присвоен в коде (либо посредством закрытия окна)
    // > Базовый метод ShowDialog завершится и этот метод вернёт данные
    // > Если процесс отменен и до заполнения переменной не дошло, она будет null
    return _authorizationResult;
}

Разработка логики выполнения запросов для ВКонтакте API


Разобравшись с авторизацией и убедившись, что она работает так, как ожидается, перейдём к запросам.


В текущем случае наиболее значимой задачей есть ограничение частоты выполнения запросов без потери производительности и с сохранением прозрачности по части распределения потоков. В процессе поиска информации выяснилось, что на эту тему существует публикация от разработчика по имени Jack Leitch, в которой он рассматривает несколько вариантов реализации такой "очереди" и завершает повествование ссылкой на код самого оптимального из них.


По традиции, начнём с описания того, как выглядит структура данных, представляющая сам запрос:


public class VKAPIGetRequestDescriptor
{
    // > Название вызываемого метода API
    public String MethodName { get; set; }

    // > Словарь параметров запроса
    public Dictionary<String,String> Parameters { get; set; }

    // > Формирование ссылки для запроса, не требующего авторизации
    public Uri GetUnauthorizedRequestUri()
    {
        var query = String.Join("&", Parameters.Select(item => String.Format("{0}={1}", item.Key, item.Value)));
        return new Uri(String.Format("https://api.vk.com/method/{0}?{1}", MethodName, query));
    }

    // > Формирование ссылки для запроса, который требует авторизации
    public Uri GetAuthorizedRequestUri(VKAPIAuthorizationResult authorizationResult)
    {
        var query = String.Join("&", Parameters.Select(item => String.Format("{0}={1}", item.Key, item.Value)));
        return new Uri(
            String.Format("https://api.vk.com/method/{0}?{1}&access_token={2}",
            MethodName,
            query,
            authorizationResult.AccessToken));
    }
}

Теперь рассмотрим логику выполнения единичного запроса к ВКонтакте API:


// > Ограничитель количества выполняемых запросов на единицу времени
private static RateGate _apiRequestsRateGate = new RateGate(2, TimeSpan.FromMilliseconds(1000));

// > Асинхронный метод выполнения запроса к API без авторизации
public static void PerformSingleUnauthorizedVKAPIGetRequestAsync(
    VKAPIGetRequestDescriptor requestDescriptor, // >> Объект запроса
    Action<Double> onDownloadProgressChanged, // >> Делегат на передачу прогресса выполнения
    Action<String> onDownloadCompleted) // >> Делегат на передачу результата выполнения
{
    // >> Вызов этого метода откладывает выполнение контекста, где он вызывался, в очередь 
    _apiRequestsRateGate.WaitToProceed();

    using (var webClient = new WebClient())
    {
        // >>> Привязка обработчика на изменение прогресса загрузки
        webClient.DownloadProgressChanged += (object sender, DownloadProgressChangedEventArgs arguments) =>
        {
            // >>>> Вызывается делегат, переданный в параметре извне
            onDownloadProgressChanged(arguments.ProgressPercentage);
        };

        // >>> Привязка обработчика на завершение прогресса загрузки
        webClient.DownloadStringCompleted += (object sender, DownloadStringCompletedEventArgs arguments) =>
        {
            // >>>> Вызывается делегат, переданный в параметре извне
            onDownloadCompleted(arguments.Result);
        };
        // >>> После привязки обработчиков запускается выполнение запроса
        webClient.DownloadStringAsync(requestDescriptor.GetUnauthorizedRequestUri());
    }
}

Обратите внимание: идея вложенных асинхронных методов априори подразумевает, что благодаря использованию замыканий реализовывать многоуровневые последовательности выполнения будет просто, а механизм передачи промежуточных параметров на пользовательский интерфейс уже фактически готов.


В свою очередь, логика пакетного выполнения множества запросов повторяет вышеуказанную в цикле:


public static void PerformMultipleAuthorizedVKAPIGetRequestsAsync(
    List<VKAPIGetRequestDescriptor> requestsDescriptors,
    VKAPIAuthorizationResult authorizationResult,
    Action<Double> onDownloadProgressChanged,
    Action<String[]> onDownloadCompleted)
{
    // > Количество переданных объектов запросов
    Int32 requestsCount = requestsDescriptors.Count();
    // > Массив, в котором хранятся значения прогресса для каждого запроса
    Int32[] progressPercentageSegments = new Int32[requestsCount];
    // > Переменная, которая хранит текущее количество выполненных запросов
    Int32 performedRequestsCount = 0;
    // > Объект для lock (для предотвращения конфликта потоков за переменную выше)
    Object performedRequestsSyncLock = new Object();
    // > Массив, в котором сохраняются фрагменты данных от каждого запроса
    String[] dataChunks = new String[requestsCount];
    // > Циклический обход списка переданных запросов
    foreach (var request in requestsDescriptors)
    {
        // >> Регулировка частоты выполнения
        _apiRequestsRateGate.WaitToProceed();

        using (var webClient = new WebClient())
        {
            webClient.DownloadProgressChanged += (object sender, DownloadProgressChangedEventArgs arguments) =>
            {
                // >>>> Так как методов было передано много, отслеживаем общий (!) процент прогресса
                progressPercentageSegments[requestsDescriptors.IndexOf(request)] = arguments.ProgressPercentage;
                // >>>> При его изменении передаём на интерфейс общее среднее арифметическое
                onDownloadProgressChanged(Convert.ToDouble(progressPercentageSegments.Sum()) / requestsCount);
            };
            webClient.DownloadStringCompleted += (object sender, DownloadStringCompletedEventArgs arguments) =>
            {
                // >>>> Сохранение фгагмента данных в массив строк
                dataChunks[requestsDescriptors.IndexOf(request)] = arguments.Result;
                // >>>> Поскольку методы выполняются асинхронно, они могут конфликтовать за одну переменную
                lock (performedRequestsSyncLock)
                {
                    // >>>>> Чтобы этого не произошло, в момент времени доступ будет иметь только один метод
                    performedRequestsCount++;
                    // >>>>> В случае, если счётчик выполненных методов равен их общему количеству
                    if (performedRequestsCount == requestsCount)
                    {
                        // >>>>>> Выполнение множества запросов можно считать завершённым
                        onDownloadCompleted(dataChunks);
                    }
                }
            };
            // >>> Запуск одного из множества запросов
            webClient.DownloadStringAsync(request.GetAuthorizedRequestUri(authorizationResult));
        }
    }
}

Готовый клиент для ВКонтакте API на .NET


Подводя итоги, можно сказать, что данная задача хоть и была не самой простой, но с соответствующим подходом для неё появилось вполне качественное решение. Всё то, о чём я написал в этой статье, заключено в отдельной библиотеке, которую можно свободно скачать из моего репозитория на GitHub и использовать в своих разработках. Само собой, пример использования (в виде тестовой программы) к ней тоже прилагается:






комментарии (58)

+11
lair ,   * (был изменён)

Эээ, "близкая к идеалу" — и на устаревшем WebClient и колбэках? А как же HttpClient, а как же TPL и async/await? А зачем использовать ручную блокировку, когда давно есть System.Collections.Concurrent?


Вы это называете "вниманием к мелочам"?

–6
IlyaTereschuk ,   * (был изменён)
WebClient гораздо более простой и подходящий вариант в данном случае, чем HttpClient, с остальным — скорее, дело вкуса и конкретной ситуации — поводом быть довольным считаю понятный код и стабильные показатели на тестировании.
+3
lair ,  
WebClient гораздо более простой и подходящий вариант в данном случае, чем HttpClient

В каком "данном случае"?


с остальным — скорее, дело вкуса и конкретной ситуации

И опять, о какой именно "конкретной ситуации" вы говорите?


поводом быть довольным считаю понятный код

Код с колбэками практически всегда хуже, чем код с async/await. И это мы еще не вдавались в детали реализации в разных контекстах. Ваше решение можно встроить в Windows Phone? В серверное приложение на asp.net?

–6
IlyaTereschuk ,  
Общая картина композиции кода была образована именно так, чтобы решение было относительно надёжным и переносимым.

В Windows Phone его можно встроить, заменив специфичное для Win32 диалоговое окно авторизации на аналог из этой платформы, серверное же приложение на ASP.NET должно будет работать по несколько другим схемам (для серверных решений есть отдельные главы в API).
+2
+3 –1
lair ,  
Общая картина композиции кода была образована именно так, чтобы решение было относительно надёжным и переносимым.

И как же вы обеспечиваете надежность и переносимость? Переносимость между чем и чем?


В Windows Phone его можно встроить, заменив специфичное для Win32 диалоговое окно авторизации на аналог из этой платформы,

То есть ваш код как есть уже использовать нельзя. Хорошо, а остальное работать точно будет?


серверное же приложение на ASP.NET должно будет работать по несколько другим схемам

… и тут ваше решение использовать нельзя. Так о какой переносимости вы говорите?


Цитата из вашего поста:


главной задачей стоит [...] обеспечить хорошую встраиваемость этого слоя как компонента для любого решения

Пока как-то выходит, что все "любые" решения ограничены Windows-приложениями.

0
polar11beer ,  
Для WP (и др. мобильных платформ) у ВК есть SDK c уже реализованной авторизацией: new.vk.com/dev/wp_sdk
0
lair ,  

Да я, собственно, и не сомневался особо.

0
Viacheslav01 ,  
Очень сильно смотрел в него, надо быть очень аккуратным используя его, без чтения кода много граблей.
+2
INC_R ,  
«Это можно выполнить только посредством вызова браузера» — не только. Можно посмотреть, как этот процесс выполняется через браузер, и сделать то же самое без него. Технически это несложно, но есть вероятность, что правилами ВК так делать запрещено.

И интереса ради, почему именно куча bool и рефлексия в VKAPIAuthorizationPermissions, почему не enum?
–3
IlyaTereschuk ,   * (был изменён)
Само собой, можно распарсить данные из браузера и выполнить авторизацию «за кулисами». Конечно же, это прямое нарушение правил и такая практика должна избегаться. В свою очередь, при обращении в техподдержку некоторые приложения могут даже получить полномочия, с наличием которых авторизация не требуется в принципе, но для этого оно должно быть действительно серьёзного уровня.

С самого начала я использовал enum, но потом вспомнил приёмы из JavaScript и заинтересовался другими возможными методами — это и привело к такой реализации, где при инициализации набора параметров можно очень лаконично указать на их присутствие либо отсутствие, а на преобразовании в другое (в этом случае, строковое) представление использовать минимум кода.
+3
INC_R ,  
Да, есть какой-то разрешенный вариант без браузера для особых случаев, но я на самом деле не очень понимаю это ограничение правил ВК. В рамках своего приложения у разработчика полный доступ к браузеру, он может и отображаемый контент подправить, и введенные пользователем данные посмотреть, и вообще что угодно. Непонятно, какой профит от ограничения.

На мой вкус, c использованием [Flags] enum более просто и наглядно. Задать нужные флаги — mask = Permissions.Offline | Permissions.Notify | Permissions.Friends |… В строку отформатировать — тоже одна-две строки кода.
+5
Seekeer ,  
Публичные поля (да ещё и с маленькой буквы). И вы это называете кодом, близким к идеальному?
0
+1 –1
dmitry_dvm ,  
Вроде бы поля и должны быть с маленькой, а свойства с большой, не?
+1
lair ,  

Не. Особенно публичные.

+4
Seekeer ,  
Во-первых, публичных полей вообще быть не должно без особой на то необходимости.
Во-вторых, традиционный стиль — все публичные члены класса начинать с большой буквы.
+6
Athari ,  

В коде вообще нарушены все конвенции и рекомендации, какие только можно нарушить.


  1. VKAPIAuthorizationSettings — нарушение правил именования: сокращения длиннее двух букв пишутся без капса (то есть должно быть VKApiAuthorizationSettings).


  2. public bool notify; — публичные поля, неверное именование, всё плохо.


  3. (bool)fieldInfo.GetValue(this) — рефлексия для работы со своим кодом и единственным классом. Костыль и тормоза.


  4. <Grid><wfi:WindowsFormsHost><wf:WebBrowser>... — обёртка гридом и окном WPF исключительно для использования WinForms.


  5. webBrowser.Navigated += WebBrowser_Navigated;NavigateError мы не обрабатываем, потому что ошибок не бывает.


  6. public new VKAPIAuthorizationResult ShowDialog() { InitializeComponent(); — скрываем метод, заменяем кодом, который нельзя вызвать два раза.


  7. return new Uri(String.Format("https://api.vk.com/method/{0}?{1}", MethodName, query)); — отличное форматирование, учитывая отсутствие не то что валидации, а даже значения по умолчанию у свойства MethodName.


  8. public Dictionary<String,String> Parameters { get; set; } коллекции конкретного типа в публичном интерфейсе с сеттером, всё плохо.


  9. public static void PerformSingleUnauthorizedVKAPIGetRequestAsync(VKAPIGetRequestDescriptor requestDescriptor, Action<Double> onDownloadProgressChanged, Action<String> onDownloadCompleted) — колбэки? А как же TPL, async/await, вот это всё? Нет, не слышали.


  10. public class VKAPIRequestsPoint — все члены статические, но класс не статический.


  11. _apiRequestsRateGate.WaitToProceed(); — ждём и висим...


  12. public static void PerformMultipleAuthorizedVKAPIGetRequestsAsync(List<VKAPIGetRequestDescriptor> requestsDescriptors, VKAPIAuthorizationResult authorizationResult, Action<Double> onDownloadProgressChanged, Action<String[]> onDownloadCompleted) — массивы и листы в публичном интерфейсе, всё плохо.

В этом коде плохо всё.

0
magicxor ,  
Что нужно использовать вместо словарей конкретного типа, массивов и листов?
0
withkittens ,   * (был изменён)
Используйте IReadOnlyList/IReadOnlyCollection, IReadOnlyDictionary. Возможно, IEnumerable, но он ничего не говорит о конечности коллекции.
0
Athari ,  

Интерфейсы: IList, ICollection, IEnumerable, IDictionary, IReadOnlyList, IReadOnlyCollection, IReadOnlyDictionary. В крайнем случае — расширяемый базовый тип, например, Collection (для словаря встроенного расширяемого базового типа нет, к сожалению). (Все типы дженериковые.) С конкретными типами коллекций и массивами много проблем: невозможность изменить тип коллекции, невозможность кэшировать из-за доступа с записью и т. п.

0
dmitry_dvm ,  
Автор не слышал про WebAuthenticationBroker?
+3
lair ,   * (был изменён)
// >> Вызов этого метода откладывает выполнение контекста, где он вызывался, в очередь 
_apiRequestsRateGate.WaitToProceed();

Кстати, нет. Вызов этого метода, как можно видеть в коде, блокирует текущий поток до наступления лучших времен. Нет, серьезно. Блокирует текущий поток. И это вызывается в цикле.


Может, я все-таки не понимаю чего-то?

–3
IlyaTereschuk ,  
Описание на 21 строке:
    ///     This class is thread safe. A single <see cref="RateGate"/> instance 
    ///     may be used to control the rate of an occurrence across multiple 
    ///     threads.
+2
lair ,  

Иии? Это как-то отменяет тот факт, что каждый вызов WaitToProceed блокирует тот поток, в котором оно вызывается? Прямо несколькими строчками выше написано:


WaitToProceed() will block the current thread until the action is allowed based on the rate limit.
–2
IlyaTereschuk ,  
И это означает, что если мы запустим несколько разных фоновых потоков, в которых происходят запросы (а экземпляр объекта, контроллирующего очередь, статичен и один на весь класс), на каждом вызове он отсрочит тот поток, который для него является в данный момент текущим таким образом, что в общей картине на момент времени их действительно «не влезет» в очередь больше, чем нужно — в этом и вся магия.
+2
lair ,  

… и эта "магия" означает, что вы блокируете n фоновых потоков (каждый из которых, вообще-то, стоит ресурсов). Вместо того, чтобы обойтись одним (и тот бы по-хорошему не блокировать).


При этом лимитируете вы именно отправку запросов, не проверяя, когда они завершаются (то есть в реальности вы можете иметь, скажем, сто одновременно висящих HTTP-запросов в очереди).

–1
IlyaTereschuk ,  
API требует именно ограничивать частоту вызовов, а если ответы на них занимают много времени, их выполнение может задерживаться сколько угодно. На каждый вызов «подвешены» асинхронные методы, которые отслеживают прогресс выполнения и отдают результат, поэтому та сторона, от которой производились вызовы, абсолютно в курсе всего, что происходит, и в ситуации компромиса между производительностью, отслеживанием хода событий и соблюдением заданных ограничений это имеет право называться разумным подходом.
+1
lair ,   * (был изменён)
и в ситуации компромиса между производительностью

Простите, а где здесь производительность? Заблокированный — не возвращенный в тредпул до нужного момента, а именно заблокированный — поток вы называете производительностью? А зачем? Что мешает использовать WaitAsync?

–4
IlyaTereschuk ,  
Вывод о том, что именно такой способ является самым оптимальным вариантом ограничения частоты вызовов был получен посредством исследований проблем на подобную тематику, которые неодноразово поднимались на StackOverflow, а занять место в решении окончательно ему дали элементарные испытания, в которых мне самому пришлось убедиться, что советуемый подход действительно работает, следует уместной логике и, как вывод, отлично вписывается в решение.
+3
lair ,   * (был изменён)
Вывод о том, что именно такой способ является самым оптимальным вариантом ограничения частоты вызовов был получен посредством исследований

Чем этот способ оптимальнее способа с WaitAsync?


PS Правильное применение WaitAsync позволило бы писать так:


var response = await _ratedHttpClient.RequestAsync(data);

Полностью неблокирующий код.

–4
IlyaTereschuk ,  
Этот метод предназначен для того, чтобы ограничивать количество одновременно выполняемых заданий, и реализует механизм «вакансий»: как только одно действие завершено, на его свободное место берётся следующее. Такой подход было бы уместно использовать в случае, когда нам нужно было бы избежать ситуации, в которой происходит интенсивная загрузка информации сразу «по нескольким фронтам». В случае же с ограничением на вызовы к API его «пристроить» не удастся, потому как есть методы, которые выполняются 50 миллисекунд, а есть методы, результат от которых нужно ждать 5 секунд. Это значит, что подобный подход не подразумевает подходящей реализации логики предсказуемой частоты вызовов. Ограничить количество вызовов в секунду и ограничить количество одновременных операций вообще — понятия разные.
0
lair ,  
Этот метод предназначен для того, чтобы ограничивать количество одновременно выполняемых заданий

Какой "этот"?

0
IlyaTereschuk ,  
Речь о WaitAsync (статья), если я сумел правильно понять то, о чём там написано
0
lair ,   * (был изменён)

Вы явно не сумели правильно понять.


По шагам.


(1) Заменяем Wait на WaitAsync в RateGate:


        public async Task<bool> WaitToProceedAsync(int millisecondsTimeout)
        {
            // Check the arguments.
            if (millisecondsTimeout < -1)
                throw new ArgumentOutOfRangeException("millisecondsTimeout");

            CheckDisposed();

            // Block until we can enter the semaphore or until the timeout expires.
            var entered = await _semaphore.WaitAsync(millisecondsTimeout).ConfigureAwait(false);

            // If we entered the semaphore, compute the corresponding exit time 
            // and add it to the queue.
            if (entered)
            {
                var timeToExit = unchecked(Environment.TickCount + TimeUnitMilliseconds);
                _exitTimes.Enqueue(timeToExit);
            }

            return entered;
        }

(код естественно не под продакшн, опущена обработка ошибок, обработка предварительного выхода и нормальный CancellationToken, можно написать на ContinueWith, если не нравится оверхед, блабла, далее аналогично).


(2) Создаем простенькую обертку вокруг HttpClient:


class RatedHttpClient
{
  RateGate _rateGate;
  HttpClient _httpClient;
  // конструктор, поддержка синглтона, если нет внешней, блаблабла

  public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
  {
    if (await _rateGate.WaitAsync().ConfigureAwait(false))
    {
      return await _httpClient.SendAsync(request).ConfigureAwait(false);
    }
    throw ...; //timeout
  }
}

(3) Используем:


private async void buttonGetBoomburumFriends_Click(object sender, RoutedEventArgs e)
{
  DisableButtons();
  var response = await singletonRatedHttpClientInstance.SendAsync(...);

  //сюда мы попадем только после того, как
  //(а) нас пропустит RateGate
  //(б) нам вернется HTTP-ответ
  //при этом
  //(ц) мы окажемся в интерфейсном потоке
  //(д) все время ожидания пунктов (а) и (б) интерфейсный поток будет свободен

  ProcessResponse(response);
  EnableButtons();
}
0
lair ,  

Кстати, "обертку", скорее всего, можно заменить на DelegatingHandler, тогда для пользователей HttpClient тротлинг будет и вовсем прозрачным.

0
IlyaTereschuk ,  
Хоть и HttpClient не подходит для задачи изначально, становится даже интересно, действительно ли такой подход сможет повлиять на производительность по факту
+1
lair ,  
Хоть и HttpClient не подходит для задачи изначально

Аргументируйте.

0
IlyaTereschuk ,  
В WebClient присутствует простой механизм отчётности прогресса выполняемого действия
0
lair ,  

… который легко повторяется на HttpClient (код ищите на SO). А теперь внимане, вопрос: насколько получаемый прогресс соответствует реальности?

0
Viacheslav01 ,  
Хоть сам берись и пиши VK SDK :)
+2
Seekeer ,  
Не стоит. Есть вполне нормальный VKNET
0
Viacheslav01 ,  
У него есть один большой недостаток, он уже написан :)
На SDK давно поглядываю, просто ради удовольствия сделать :)
Ну и потом огрести от хабра :)
0
Sybe ,  
кажется, там нет асинхронного API
0
TreyLav ,  
Как я понял, планируют сделать.
0
Viacheslav01 ,  
Я уже поковырял, в общем то интересно, но местами странно.
0
IvanPanfilov ,  
если это
> Близкая к идеалу адаптация ВКонтакте API

то мне даже немного жаль C# програмистов
+3
withkittens ,   * (был изменён)
Нет, это не оно. Поэтому у C#-программистов по-прежнему всё хорошо ;)
0
Scrooge2 ,  
Уже как-то был на хабре создатель «идеальных» контролов для WPF.
Тут уровень чуток выше.
+1
pnick ,  
Хочешь, чтобы большое сообщество проаудировало твой код? Не проси про это прямо — не ответят или попросят денег.
Закинь на Хабр с заголовком «Близкий к идеалу код» и немного поотвечай в комментариях.
0
IlyaTereschuk ,  
И дьявола вызовут
image
0
pawlo16 ,  
Логика формирования запроса для авторизации готова, теперь можно заняться ей самой. Насколько нам уже известно, для этого требуется диалог на WPF со встроенным браузером из библиотеки Windows Forms:

Ну и чего ради этот треш со встроенным ослобраузером, если далее у вас

using (var webClient = new WebClient())
{

? Вы вообще понимаете, что авторизацию можно сделать через WebClient без дополнительной гимнастики, или это у вас такой подход «максимально всё запутать»?
0
IlyaTereschuk ,  
Делать клиентскую авторизацию вне браузера — против правил ВК
0
pawlo16 ,  
Клиентская авторизация через браузер — это всего лишь POST запрос c некоторыми специфическими хедерами, структуру которого элементарно воспроизвести с помощью локального веб-прокси. Но с учётом упомянутых выше стилистических и семантических ляпов вам конечно проще впихнуть дополнительную зависимость и не париться
0
IlyaTereschuk ,  
Но ВК действительно запрещает делать авторизацию вне его интерфейса
+1
lair ,  

Вообще-то, не только воспроизвести, но и поддерживать с каждым изменением на стороне провайдера авторизации — а те, в свою очередь, совершенно не хотят, чтобы кто-то авторизовался мимо браузера, поскольку это нарушает идею OAuth (точнее говоря, в OAuth для тех случаев, когда предполагается авторизация без браузера, используется client credentials flow).

0
pawlo16 ,  
Да, именно так. В случае успеха авторизации ответ содержит строку-токен клиентской сессии. Я использовал такой подход с некоторыми известными веб сервисами, например, betfair.com, и не не припомню что-бы что-то там менялось. Вряд ли vc.com исключение
0
lair ,  

Я предпочел бы не ставить свое приложение в зависимость от того, решит ли поставщик авторизации слегка поменять форму логина. В OAuth действительно нормально использовать браузер (в зависимости от степени паранойи — встроенный или внешний),

0
pawlo16 ,  
Скорее ВК изменит свой API)) в этом случае так же придётся вносить изменения в программу. Я бы предложил продумать логику автообновления дабы закрыть этот вопрос
0
lair ,   * (был изменён)

API — это внешний контракт, он предполагается стабильным. А страница сайта контрактом не является.


И нет, автообновление, особенно в мобильном приложении — не решение.


(Я уж не говорю о том, что вы нарушаете безопасность пользователя, и за такое можно и в бан попасть)