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.
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.