可以根据条件重试一次吗?

时间:2015-11-04 02:39:15

标签: c# system.reactive

我有一个有效的方法,看起来基本上是这样的:

public IObservable<List<Stuff>> GetGoodStuff()
{
    return Observable.FromAsync(GetAccessTokenAsync)
        .SelectMany(accessToken =>
        {
            return httpClient.SendAsync(request);
        })
        .SelectMany(response => 
        { 
            response.EnsureSuccessStatusCode(); 
            return response.Content.ReadAsStringAsync(); 
        })
        .Select(json => 
        {
            return JsonConvert.DeserializeObject<List<Stuff>>(json);
        });
}

“GetAccessTokenAsync”返回api的缓存访问令牌,或者,第一次将获取令牌。其余部分在httpclient和Rx方面非常标准。

这就是事情:我想捕获401错误,更新访问令牌,然后重试整个事情。但只有一次 - 之后它可以将异常抛给调用者。

在那个中间区块,我可以这样做:

            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                InvalidateAccessToken();
                // what now???
            }

但那又怎么样?没有看到递归调用如何工作。以某种方式包裹整个事情?还没有看到它......

编辑1 - 7Nov2015

这个日期的答案都很好看。更具声明性的方法似乎不是一个变化,能够隐藏大部分“管道”,但我无法在所有场景中使用它。

所以基于@Timothy Shields的建议,我想出了这个很好看并且很好地隐藏了管道(哦,是的,它的工作原理: - )

/// <summary>
/// Makes an httpclient request using the access token. If Unauthorized is received the access
/// token will be reacquired and the request will be retried once.
/// </summary>
/// <returns>The json result from a successful request.</returns>
async Task<string> MakeRequestWithAccessToken(string requestUri, CancellationToken cancellationToken)
{
    const int RetryCount = 1;

    HttpResponseMessage response = null;
    for (int i = 0; i <= RetryCount; i++)
    {
        var accessToken = await GetAccessTokenAsync();

        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
        request.Headers.Add("Authorization", "Bearer " + accessToken);

        var client = new RemoteService(ApiUrl).NewClient();

        response = await client.SendAsync(request, cancellationToken);
        if (i < RetryCount && response.StatusCode == HttpStatusCode.Unauthorized)
        {
            InvalidateAccessToken();
            continue;
        }

        response.EnsureSuccessStatusCode();
    }

    return await response.Content.ReadAsStringAsync();
}

public IObservable<List<Stuff>> GetGoodStuff(int maxCount)
{
    return Observable.FromAsync(async cancellationToken =>
    {
        var requestUri = string.Format("mypath.json?count={0}", maxCount);
        var json = await MakeRequestWithAccessToken(requestUri, cancellationToken);
        return JsonConvert.DeserializeObject<List<Stuff>>(json);
    });
}

2 个答案:

答案 0 :(得分:3)

您应该使用async - await来执行此操作:

public IObservable<List<Stuff>> GetGoodStuff()
{
    return Observable.FromAsync(async cancellationToken =>
    {
        const int RetryCount = 1;
        for (int i = 0; i <= RetryCount; i++)
        {
            var accessToken = await GetAccessTokenAsync();
            var request = MakeRequest(accessToken);
            var response = await httpClient.SendAsync(request, cancellationToken);
            if (i < RetryCount && response.StatusCode == HttpStatusCode.Unauthorized)
            {
                InvalidateAccessToken();
                continue;
            }
            response.EnsureSuccessStatusCode();
            var json = await response.Content.ReadAsStringAsync(cancellationToken);
            return JsonConvert.DeserializeObject<List<Stuff>>(json);
        }
    });
}

这种技术允许您编写标准命令式代码,公开为好IObservable<T>

请注意,我正在猜测你的&#34;重试&#34;将要看。在调用InvalidateAccessToken()后,您不清楚自己想要做什么,所以我猜测并发明了MakeRequest方法。您应该很容易将其调整为完全符合您要求的代码。

答案 1 :(得分:2)

我已经假设InvalidateAccessToken也是异步,就像GetAccessTokenAsync一样。

解决方案将触发失效,并通过抛出允许重试触发的异常继续。如果请求第二次失败,则失效序列将只重放异常,该异常将冒泡到订阅者。

public IObservable<List<Stuff>> GetGoodStuff()
{
    var invalidate = Observable.FromAsync(InvalidateAccessTokenAsync)
                .Select(x => Observable.Throw<string>(new Exception()))
                .Switch()
                .Replay()
                .RefCount();

    return Observable.FromAsync(GetAccessTokenAsync)
        .SelectMany(accessToken =>
        {
            return httpClient.SendAsync(request);
        })
        .SelectMany(response => 
        { 
            if (response.StatusCode == HttpStatusCode.Unauthorized)
            {
                return invalidate;
            }

            response.EnsureSuccessStatusCode(); 
            return response.Content.ReadAsStringAsync().ToObservable(); 
        })
        .Select(json => 
        {
            return JsonConvert.DeserializeObject<List<Stuff>>(json);
        })
        .Retry(1);
}

编辑:回答@supertopi问题

invalidate序列中的Select返回IOberservable<IOberservable<string>>。我们只对内部序列感兴趣所以我使用Switch运算符移动到内部序列。

Replay运算符返回IConnectableObservable<string>,它会在订阅时重放其来源的值。 IConnectableObservable<T>基本上允许我们共享数据值,并且在重放值时,Observable.FromAsync(InvalidateAccessTokenAsync)只会被调用一次。任何迟到的订阅者只会看到重播的值,这将是例外。请查看introintorx上的definition以获得详尽的解释。

如果没有RefCount运算符,我需要在IConnectableObservable上手动调用connect。我使用RefCount为我处理连接并将序列转换回IObservable<string>。再次可以找到更多信息here