1
votes

I need to make multiple webrequests where URIs are in a DataTable. Earlier I had the below code. But I realized this makes synchronous calls as await would wait till GET/POST call is complete and response is processed then it proceeds to next iteration.

foreach (DataRow dr in dt.Rows)
{
    activeTasks.Add(SendRequestAsync(dr));
    Task.WhenAll(activeTasks).Wait();
}

private async Task<string> SendRequestAsync(DataRow dr)
{
    using (var client = new HttpClient())
    {
        string reqMethod = (dr["RequestMethod"] != null && dr["RequestMethod"].ToString() != "") ? dr["RequestMethod"].ToString() : "GET";
        client.BaseAddress = new Uri(dr["URL"].ToString());
        client.DefaultRequestHeaders.Accept.Clear();
        string reqContentType = (dr["RequestContentType"] != null && dr["RequestContentType"].ToString() != "") ? dr["RequestContentType"].ToString() : "text/xml";
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(reqContentType));

        HttpResponseMessage response = null;
        try
        {
            if (reqMethod == "GET")
                response = await client.GetAsync(client.BaseAddress.AbsoluteUri);
            else
                response = await client.PostAsync(client.BaseAddress.AbsoluteUri, null);

            response.EnsureSuccessStatusCode();
            var responseText = await response.Content.ReadAsStringAsync();
            return responseText;
        }
        catch (Exception e)
        {
            return "-1";
        }
    }
}

Then I came across Parallel feature and used Parallel.ForEach instead. Like this:

Parallel.ForEach(rows, dr =>
{
    activeTasks.Add(SendRequestAsync(dr));
    Task.WhenAll(activeTasks).Wait();
});

This works fine, parallelism is achieved, requests are asynchronous and it completes within fraction of a time as compared to earlier solution. But the problem is it is not reliable - at times I get errors like

  • System.IndexOutOfRangeException: Index was outside the bounds of the array
  • System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Is there anyway we can achieve http async calls within a foreach?

2
I wouldn't use the Parallel.ForEach for this. Really you just want to move your Task.WhenAll outside of the loop that you are adding tasks to your task list in. - Jonathon Chase
When using parallel foreach you want to make sure you use threadsafe collections from the concurrent collections namespace docs.microsoft.com/en-us/dotnet/api/… - DetectivePikachu
As an aside, if you do decide to make your calls concurrently, you may need to increase the timeout on each call, as you may have a request blocked by the maximum number of simultaneous sockets being open in some environments. - Jonathon Chase
From the documentation: HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. - Theodor Zoulias
Thanks all for your responses, I upvoted everyone :) - Vijay

2 Answers

7
votes

As @Johnathon_Chase said, just move your WhenAll() call outside of the loop:

foreach (DataRow dr in dt.Rows)
{
    activeTasks.Add(SendRequestAsync(dr));
}
Task.WhenAll(activeTasks).Wait();

The for loop populates the collection, and then Task.WhenAll() blocks while the requests complete.

4
votes

Parallel.ForEach is for CPU-intensive operations and is not designed for I/O-intensive operations or for async.

You can await inside of a foreach loop just fine. The method containing your loop needs to be async itself, though.