Как использовать Polly с Flurl.Http?

В настоящее время у меня есть такой запрос:

await url
    .SetQueryParams(queryString)
    .SetClaimsToken()
    .GetJsonAsync<T>()

Я хочу начать использовать Polly (https://github.com/App-vNext/Polly) теперь для обработки повторных попыток и повышения удобства работы пользователей. Например, не «зависает» от пользователя с первой попытки из-за плохого сетевого подключения. Это пример, который я пытался использовать:

int[] httpStatusCodesWorthRetrying = { 408, 500, 502, 503, 504 };
Policy
    .Handle<HttpException>()
    .OrResult<HttpResponse>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode))
    .WaitAndRetryAsync(new[] {
                    TimeSpan.FromSeconds(1),
                    TimeSpan.FromSeconds(2),
                    TimeSpan.FromSeconds(3)
                })
    .ExecuteAsync( await url... )

Но для этого требуется, чтобы HttpResponse был возвращаемым типом. Как вы можете видеть из моего примера с Flurl, он возвращает T, хотя это HttpResponse. T - это просто тип, используемый для десериализации StringContent.

Этот первый пример вообще не работает, поскольку я использую его внутри PCL и не могу найти там ссылку на System.Web. Итак, я попробовал это:

Policy
    .HandleResult(HttpStatusCode.InternalServerError)
    .OrResult(HttpStatusCode.BadGateway)
    .OrResult(HttpStatusCode.BadRequest)
    .WaitAndRetryAsync(new[] {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(3)
    })
    .ExecuteAsync(async () =>
    {
        await url...
    });

Но и этот не работает, потому что Полли ожидает HttpStatusCode в качестве возвращаемого типа. Итак, мой вопрос: как я могу сказать Polly обработать эти HttpStatusCode и по-прежнему разрешить мой возврат типа T?


person eestein    schedule 22.11.2016    source источник
comment
Вы также можете зарегистрировать глобальную политику Polly, которую Flurl будет использовать по умолчанию при каждом обращении.   -  person Todd Menier    schedule 11.09.2018


Ответы (3)


Полли может интерпретировать любое значение, возвращаемое делегатом, выполняемым через политику, как ошибку. Однако, как вы заметили, вызов .GetJsonAsync<T>() в опубликованном примере:

await url
    .SetQueryParams(queryString)
    .SetClaimsToken()
    .GetJsonAsync<T>()

возвращается T. Вызов скрывает HttpResponseMessage, переходя прямо к десериализации Json в T.

Вам нужно будет использовать перегрузку в flurl, которая возвращает что-то около HttpResponseMessage. Я не использовал flurl, но это перегрузка, возвращающая Task<HttpResponseMessage>, выглядит многообещающе. Вероятно, вы могли бы сделать что-то вроде:

List<int> httpStatusCodesWorthRetrying = new List<int>(new[] {408, 500, 502, 503, 504});
HttpResponseMessage response = await Policy
    .Handle<HttpRequestException>() 
    .Or<OtherExceptions>() // add other exceptions if you find your call may throw them, eg FlurlHttpException
    .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains((int)r.StatusCode))
    .WaitAndRetryAsync(new[] {
                    TimeSpan.FromSeconds(1),
                    TimeSpan.FromSeconds(2),
                    TimeSpan.FromSeconds(3)
                })
    .ExecuteAsync(() => 
       url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetAsync()
    );

T responseAsT = await Task.FromResult(response).ReceiveJson<T>();

Вызов .ReceiveJson<T>() в конце предлагается просто для сравнения исходного кода flurl для вашего исходного вызова .GetJsonAsync<T>() здесь с замененным .GetAsync(); здесь.

Конечно, вы можете обернуть все это в краткий вспомогательный метод расширения на flurl, возможно, примерно так:

async T GetJsonAsyncResiliently<T>(this IFlurlClient client, Policy policy) // OR (if preferred): this Url url instead of IFlurlClient client
{
    return await Task.FromResult(policy.ExecuteAsync(() => client.GetAsync())).ReceiveJson<T>();
}

ИЗМЕНИТЬ. Возможно, я указал на неправильные перегрузки flurl для вашего случая, указывая на методы на IFlurlClient. Однако в flurl на Url и string существует параллельный набор методов расширения, поэтому применяются те же принципы.

person mountain traveller    schedule 22.11.2016
comment
@eestein После загрузки flurl внесены некоторые незначительные изменения в синтаксис. Это тебя рассортировало? - person mountain traveller; 23.11.2016
comment
Спасибо за ответ, сейчас протестирую! - person eestein; 23.11.2016
comment
Спасибо, я смог адаптировать свой код на ваших примерах! :) - person eestein; 23.11.2016

Вам не нужно отказываться от использования удобных методов, таких как GetJsonAsync<T>(), потому что Flurl выдает исключение для ответов, отличных от 2XX (или, как бы то ни было, вы настроить его), что должно позволить ему очень хорошо работать с Полли. Просто удалите части .Handle<HttpException> и .OrResult<HttpResponse> из исходного кода и обработайте вместо них FlurlHttpException:

T poco = await Policy
    .Handle<FlurlHttpException>(ex => httpStatusCodesWorthRetrying.Contains((int)ex.Call.Response.StatusCode))
    .WaitAndRetryAsync(...)
    .ExecuteAsync(() => url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetJsonAsync<T>());

И просто предложение по дальнейшей очистке:

T poco = await Policy
    .Handle<FlurlHttpException>(IsWorthRetrying)
    .WaitAndRetryAsync(...)
    .ExecuteAsync(() => url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetJsonAsync<T>());

private bool IsWorthRetrying(FlurlHttpException ex) {
    switch ((int)ex.Call.Response.StatusCode) {
        case 408:
        case 500:
        case 502:
        case 504:
            return true;
        default:
            return false;
    }
}
person Todd Menier    schedule 23.11.2016
comment
Красиво, Тодд! Меня тоже интересовал такой подход. Я изначально не рекомендовал, так как не был уверен, что все вызовы flurl гарантируют возврат FlurlHttpException. Например, github.com/tmenier /Flurl/blob/master/src/Flurl.Http.Shared/ (общедоступный) не ловит? Но согласитесь, этот шаблон действительно хорош с .GetJsonAsync<T>()). Хорошая библиотека, кстати. - person mountain traveller; 23.11.2016
comment
@mountaintraveller Если вызов завершен, а ответ содержит статус неуспеха, FlurlHttpException виртуально гарантирован. По крайней мере, это предполагаемое, задокументированное поведение. Но если это беспокоит, то, конечно, не повредит оставить там другие конфигурации Полли. (Спасибо, и ваша библиотека тоже выглядит очень мило! Это дает мне идеи для добавления повторных попыток во Flurl :)) - person Todd Menier; 23.11.2016
comment
@mountaintraveller, кстати, выделенная вами строка не улавливает ошибку, потому что она попадает в ловушку / повторно выбрасывается как FlurlHttpExcepton глубже в FlurlMessageHandler - person Todd Menier; 23.11.2016
comment
@ToddMenier Отлично! Я не заметил обычай HttpMessageHandler, зарегистрированный на HttpClient заводом по умолчанию. Этот github.com/tmenier /Flurl/blob/master/src/Flurl.Http.Shared/ было действительно приятным чтением! - person mountain traveller; 24.11.2016
comment
@ToddMenier, этот код должен компилироваться? Я получаю сообщение «Невозможно преобразовать задачу T в задачу ‹HttpResponseMessage› для ExecuteAsync. - person Robin; 14.10.2019
comment
@Robin да, он должен компилироваться. В моем примере вообще не используется HttpResponseMessage, вы уверены, что следуете ему правильно? - person Todd Menier; 15.10.2019
comment
@ToddMenier, спасибо, я обнаружил проблему, я добавил OrResult перед ожиданием, и он меняет его на HTTPResponseMessage в качестве возвращаемого типа. Кстати, IsWorthReTrying необходимо выполнить нулевую проверку для .Call, я получаю нулевое исключение, если URL-адрес недействителен. - person Robin; 15.10.2019

Настройте Flurl, установив HttpClientFactory, который можно настроить с помощью Polly, и создайте собственный HttpClientFactory:

public class MyCustomHttpClientFactory : DefaultHttpClientFactory, IMyCustomHttpClientFactory
{
    private readonly HttpClient _httpClient;
    public MyCustomHttpClientFactory(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    public override HttpClient CreateHttpClient(HttpMessageHandler handler)
    {
        return _httpClient;
    }
}

Зарегистрируйте эту Службу в ConfigureServices с помощью:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddHttpClient<IMyCustomHttpClientFactory, MyCustomHttpClientFactory>()
        .SetHandlerLifetime(...)
        .AddPolicyHandler(....);
}

И назначьте эту фабрику Flurl:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Get HttpClientFactory and Configure Flurl to use it.
    var factory = (IMyCustomHttpClientFactory)app.ApplicationServices.GetService(typeof(IMyCustomHttpClientFactory));
    FlurlHttp.Configure((settings) => settings.HttpClientFactory = factory);
}
person ThomasG    schedule 11.09.2018