多个异步调用,如何以有意义的方式处理响应

Multiple Async calls, how to deal with responses in a way that make sense

我正在对几个不同的 URL 进行多次异步调用,两个 url 应该 return 相同的结果,但我想比较两者的结果或检查中的某些值回应。我不确定如何在状态代码之外的响应中比较或查找特定值,有没有一种简单的方法可以做到这一点?还想注意响应,如果它失败了,我希望能够在我的代码中跟踪它,以便不再使用 url,我不确定我将如何处理这个

代码:

private async Task<ClientModel> getClientInfoAsync(string clientID)
    {
        
        ClientModel c = null;
       
        try
        {
            
            var client = new HttpClient();
            //Start requests for all of them
            var requests = urls.Select
                (
                url => client.GetAsync(getURL(url, "Client", clientID))
                ).ToList();
            //Wait for all the requests to finish
            await Task.WhenAll(requests);

            //Get the responses
            var responses = requests.Select
                (
                    task => task.Result
                );
           
            foreach (var r in responses)
            {
                
                // Extract the message body
                var s = await r.Content.ReadAsStringAsync();                    
                          
                if (r.IsSuccessStatusCode)
                {
                    c = r.Content.ReadAsAsync<ClientModel>().Result;                        
                    SetLastSuccessfulCommunicationDetails();  //after this call HERE I THINK IS WHERE I WOULD COMPARE RESPONSES AND GO FROM THERE                     

                }
                
            }
           
        }
        catch (Exception ex)
        {
            string errMsg = "Error getting the client info";
            //...catch error code here...
        }
        
        return c;
    }

基本上我不确定如何处理响应,根据我的比较和响应状态,只有 return 一个客户端模型 (c)。让我知道是否需要包含任何进一步的信息

首先,如果可以的话,请不要使用.Result。有一种更简洁的方式来获取所有回复。

var responses = await Task.WhenAll(requests);

该方法的一个缺点是,如果任何请求抛出异常,它就会抛出异常。所以如果你想智能地处理这种情况,你需要 try/catch 并有一些特殊的逻辑来处理它。

您可以为每个异步步骤继续该模式,以将结果作为集合处理:

        var validResponses = responses.Where(r =>r.IsSuccessStatusCode);
        var clientModels = await Task.WhenAll(
            validResponses
                .Select(async r => await r.Content.ReadAsAsync<ClientModel>()));

这种方法的一个缺点是您最终会等待所有请求完成,然后才能开始阅读任何响应内容。因此,将发出请求、获取响应和沿途收集任何数据的整个过程放在一个异步方法中,并且只说 var responseData = await Task.WhenAll(urls.Select(ThatMethod)) 可能更有意义。这样,只要有一个请求返回,您就可以开始使用它的响应。

在比较结果时,如果不知道您正在寻找哪种比较,就无法提供帮助。但是,假设您想要模型中某些 属性 具有最高值的那个,例如:

        var lastResponseModel = clientModels
            .OrderByDescending(c => c.LastSaved)
            .First();
        return lastResponseModel;

如果我可以假设您有一个如下所示的方法:

private Task<ClientModel> DetermineClientModelFromResponses(IEnumerable<string> responses)

...然后您可以使用 Microsoft 的 Reactive Framework(又名 Rx)- NuGet System.Reactive 并添加 using System.Reactive.Linq;.

它让你这样做:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await DetermineClientModelFromResponses(
        await Observable.Using(
            () => new HttpClient(),
            client =>
                urls
                    .ToObservable()
                    .SelectMany(url => Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID))))
                    .Where(response => response.IsSuccessStatusCode)
                    .SelectMany(response => Observable.FromAsync(() => response.Content.ReadAsStringAsync()))
                    .ToArray()));
                

...或者,或者,这个:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await DetermineClientModelFromResponses(
        await Observable.Using(
            () => new HttpClient(),
            client =>
            (
                from url in urls.ToObservable()
                from response in Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID)))
                where response.IsSuccessStatusCode
                from text in Observable.FromAsync(() => response.Content.ReadAsStringAsync())
                select text
            ).ToArray()));

如果您对第一次成功响应获胜感到满意,那么这应该适合您,但您需要确保至少有一个成功:

private async Task<ClientModel> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.FromAsync(() => client.GetAsync(getURL(url, "Client", clientID)))
            where response.IsSuccessStatusCode
            from text in Observable.FromAsync(() => response.Content.ReadAsAsync<ClientModel>())
            select text
        ).Take(1));

为了使其对错误更加稳健,您有一些策略。

我改造了你的代码来做一个简单的例子:

async Task Main()
{
    var result = (string)"no result";
    try
    {
        result = await GetClientInfoAsync("123");
    }
    catch (NotImplementedException ex)
    {
        Console.WriteLine(ex.Message);
    }
    Console.WriteLine(result);
}

private List<string> urls = new List<string>() { "Hello" };

private async Task<string> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.FromAsync(() => Test1(url))
            from text in Observable.FromAsync(() => Test2(response))
            select $"{clientID}:{text}"
        )
            .Concat(Observable.Return<string>(null))
            .Take(1));

private Random _random = new Random();

Task<string> Test1(string url)
{
    if (_random.NextDouble() > 0.3)
    {
        throw new NotImplementedException("Test1!");
    }
    return Task.Run(() => $"{url}!");
}

Task<string> Test2(string response)
{
    if (_random.NextDouble() > 0.3)
    {
        throw new NotImplementedException("Test2!");
    }
    return Task.Run(() => $"{response}#");
}

此代码将在出现异常时立即结束 GetClientInfoAsync,并让它冒泡到 Main 方法。这对你来说可能不够。

一种替代方法是向每个 Test1Test2 添加正常的 try/catch 代码,以确保它们永远不会失败。

或者,您可以很容易地添加“重试”功能。

private async Task<string> GetClientInfoAsync(string clientID) =>
    await Observable.Using(
        () => new HttpClient(),
        client =>
        (
            from url in urls.ToObservable()
            from response in Observable.Defer(() => Observable.FromAsync(() => Test1(url))).Retry(5)
            from text in Observable.Defer(() => Observable.FromAsync(() => Test2(response))).Retry(5)
            select $"{clientID}:{text}"
        )
            .Concat(Observable.Return<string>(null))
            .Take(1));

请注意,现在 Test1Test2 各重试 5 次。

错误仍然有可能通过,但这是正常的编码,对吗?

请注意,我还添加了 .Concat(Observable.Return<string>(null)) 以确保在没有值来自查询本身时查询生成一个值。 Concat 在连接 null 结果之前等待主查询结束,因此如果主查询没有产生任何值,那么 null 将出来。