3

The following code is a program.

  • The payloadList contains json-objects like {"id": 1, "foo": "one" }.
  • Each object of payloadList should be sent to a server with httpClient.SendAsync()
  • The response for each request should be stored in responseList

The code below does partly work. But i do not understand why some parts are not working. I assume that the responses are not completed when responseList.Add(foo) is executed.

This request shall be send for each json-object {"id": 1, "foo": "one" }

public static async Task<string> DoRequest(HttpClient client, string payload)
{   
    var request = new HttpRequestMessage(HttpMethod.Post, 
                           "http://httpbin.org/anything");
    request.Content = new StringContent(payload
                           , Encoding.UTF8, "application/json");        
    var response = await client.SendAsync(request); 
    string responseContent = await response.Content.ReadAsStringAsync(); 
    return responseContent;
}

The DoRequest()-method wraps the request and can be used inside main like this

static async Task Main()
{
    var responseList = new List<string>();  
    var payloadList = new List<string>{"{ 'id': 1, 'bar': 'One'}",
                                       "{ 'id': 2, 'bar': 'Two'}",
                                       "{ 'id': 3, 'bar': 'Three'}"};
        
    var client = new HttpClient();
    
    payloadList.ForEach(async (payload) => {
        var responseFoo = await DoRequest(client, payload);
        responseFoo.Dump("response"); // contains responseFoo
        responseList.Add(responseFoo);  // adding responseFoo to list fails
    });                     
    responseList.Dump();    // is empty
}

The responseList is empty.

  • Expected responseList.Dump() contains all responses responseFoo.
  • Actual responseList is empty.

Linqpad-demo

Questions

  • How can each response for await client.SendAsync(request) be added to a responseList?
  • Why is responseList empty despite that foo.Dump() works?
  • How to confirm or check if every client.SendAsync is finished?
  • Would you write the code above different - why?
surfmuggle
  • 5,527
  • 7
  • 48
  • 77
  • ask one question at a time please. See also [ask] – ADyson Dec 08 '22 at 14:14
  • `responseList.Add(foo); // this not `...in what way does it "not work"? Is there an error? – ADyson Dec 08 '22 at 14:14
  • The responseList is empty. Expected `responseList.Dump()` contains all responses `foo`. Actual `responseList` is empty. – surfmuggle Dec 08 '22 at 14:16
  • P.S. putting `responseList.Dump();` outside the foreach loop obviously won't work because that'll run before all the async stuff is complete. – ADyson Dec 08 '22 at 14:16
  • https://stackoverflow.com/questions/25009437/running-multiple-async-tasks-and-waiting-for-them-all-to-complete may be helpful – ADyson Dec 08 '22 at 14:17
  • @Dyson how can i await that all the async stuff is done? – surfmuggle Dec 08 '22 at 14:20
  • I already provided a link which might be helpful, if the answer below doesn't address your issue (although I think the latest edits to it are basically describing the same approach using Task.WhenAll) – ADyson Dec 08 '22 at 14:33

1 Answers1

4

List.ForEach is not Task-aware and will execute everything in parallel without waiting for the results (i.e. it will create tasks for all items in payloadList and will continue to the next statement responseList.Dump(); without awaiting them).

  1. In newer versions of .NET you can use Parallel.ForEachAsync(for example as in this answer) combined with use of appropriate collection from System.Collections.Concurrent, for example ConcurrentBag<T>. List is not thread-safe and modifying it concurrently can lead to a lot of problems.

    Code can look like the following:

    var responseList = new ConcurrentBag<string>();
    
    await Parallel.ForEachAsync(payloadList, async (payload, ct) =>
    {
        var foo = await DoRequest(client, payload, ct);
        responseList.Add(foo);     
    });
    
    static async Task<string> DoRequest(HttpClient client, string payload, CancellationToken ct)
    {   
        var request = new HttpRequestMessage(HttpMethod.Post, "http://httpbin.org/anything");
        request.Content = new StringContent(payload, Encoding.UTF8, "application/json");        
        var response = await client.SendAsync(request, ct); 
        string responseContent = await response.Content.ReadAsStringAsync(ct); 
        return responseContent;
    }
    
  2. If you are fine with all requests running in parallel - just create a enumerable of tasks and use Task.WhenAll<T>():

    var tsks = payloadList
        .Select(payload => DoRequest(client, payload));
    
    string[] result = await Task.WhenAll(tsks);
    
  3. If you want to execute requests one after another - just switch to ordinary foreach:

    var responseList = new List<string>(); // no need for concurrent collection in this case 
    
    foreach (var payload in payloadList)
    {
        var foo = await DoRequest(client, payload);
        responseList.Add(foo);  
    }  
    

If you want to dive deeper here some links:

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Make sure to await `Parallel.ForEachAsync` – apc Dec 08 '22 at 14:18
  • Could you add some code how to use it? – surfmuggle Dec 08 '22 at 14:21
  • Thanks for the code. One more thing. I would expect that `responseList.Add(foo)` should work. But it stays empty. Why is this the case? – surfmuggle Dec 08 '22 at 14:28
  • 1
    @surfmuggle Potentially you app finishes before the requests are completed. And `responseList.Dump();` is executed before any request is completed with very high chance. I.e. `async`-`await` inside `List.ForEach` has no effect. – Guru Stron Dec 08 '22 at 14:31
  • The part `Parallel.ForEachAsync()` gets the error CS0117 'Parallel' does not contain a definition for 'ForEachAsync' – surfmuggle Dec 08 '22 at 14:35
  • @surfmuggle then you are using older version of framework. See the updated code - the other two approaches will work. – Guru Stron Dec 08 '22 at 14:36
  • 1
    @surfmuggle added some links which can be useful if you will want to go a bit deeper into the topic. – Guru Stron Dec 08 '22 at 14:48