Search code examples
c#.netasync-awaitresponse

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


I am making multiple async calls to a couple of different URLs, both URLs should return the same result but I would like to compare the results from both or check for certain values in the responses. I am not sure how to compare or look for specific values in the responses outside of status codes, is there an easy way to do this? also would like to take note of the response and if it was a failure I want to be able to keep track of that later in my code to not use that URL again and I'm not sure how I would go about this.

Code:

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;
    }

Basically I'm unsure of how to deal with the responses and only return one client model (c) based on my comparison and status of the response. Let me know if I need to include any further information.


Solution

  • If I can assume that you have a method that looks like this:

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

    ...then you can use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive and add using System.Reactive.Linq;.

    It let's you do this:

    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()));
                    
    

    ...or, alternatively, this:

    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()));
    

    If you're OK with first successful response wins, then this should work for you, but you need to ensure you have at least one succeed:

    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));
    

    To make this more robust to errors you have a few strategies.

    I remodelled you code to make a simple example:

    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}#");
    }
    

    This code will end GetClientInfoAsync as soon as there's an exception and it lets it bubble up to the Main method. That might not be sufficient for you.

    One alternative is to add normal try/catch code to each of Test1 and Test2 to ensure they never fail.

    Alternatively, you can add "try again" functionality quite easily.

    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));
    

    Note that now Test1 and Test2 retry 5 times each.

    There's still a chance that the error gets through, but that's normal coding, right?

    Note that I also added .Concat(Observable.Return<string>(null)) to ensure that the query produces one value if no values comes from the query itself. The Concat waits for the main query to end before it concatenates the null result, so if the main query produces no values then null will come out.