0

I've a method called by my controller and i'm trying to add new value during the iteration

public async Task<MyResult> GetItems(int itemId) {
try
{
    //returns a lists of items
    var resList = await _unit.repository.GetAll(x => x.ItemId.Equals(itemId));

    if(resList.Count() == 0)
        throw new Exception(err.Message);

    //Here i need to list the sub-items, but the method returns async
    resList.ToList().ForEach(async res => {
        switch (res.Type)
        {
            case Itemtype.None :
                res.SubItems = await _unit.repository.GetAll(y => y.ItemId(res.ItemId));
            break;
            case Itemtype.Low :
                //get from another place
            break;
        } 
    });

    return new MyResult { 
        res = resList
    };
}
catch (Exception ex)
{
    throw ex;
}

}

It was shown my Items, but without the sub-items

  • As it is, you are iterating over a temporary list (created by `ToList()`) and storing the subitems there, rather than in the original `reslist`. You can fix this if you replace `ToList().ForEach` with a simple `foreach` loop that updates `resList` directly. – John Wu Jun 09 '20 at 21:12
  • What is `ForEach` method? – Guru Stron Jun 09 '20 at 21:15
  • @JohnWu but `ToList` is not creating new `res`'s it is modifying existing ones. I think switching to `foreach` will help but because code will actually `await` for result and will not just create `resList.Count()` of tasks and return. – Guru Stron Jun 09 '20 at 21:17
  • `List.ForEach` + async = [async void](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void). Check out Gabriel Luci's answer in this question: [Async void lambda expressions](https://stackoverflow.com/questions/61827597/async-void-lambda-expressions/61828401#61828401) – Theodor Zoulias Jun 10 '20 at 10:06

3 Answers3

3

Note that using await inside a foreach loop will pause the iteration until the Task completes.

This can lead to pretty poor performance if you have a large number of items.

Allow the tasks to run simultaneously:

var tasks = resList.Select(async res =>
{
    switch (res.Type)
    {
        case Itemtype.None :
            res.SubItems = await _unit.repository.GetAll(y => y.ItemId(res.ItemId));
        break;
        case Itemtype.Low :
            //get from another place
        break;
    } 
});

Then use Task.WhenAll to allow them all to complete:

await Task.WhenAll(tasks);

Enumerable.Select works differently to List<T>.ForEach in that it returns from the provided delegate, which in this case is Task.

Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
1

It's important to realize that the await keyword will return when it acts on an incomplete Task. Usually it returns its own incomplete Task that the caller can use to wait until it's done, but if the method signature is void, it will return nothing.

Whatever that ForEach() is, it's likely that it does not accept a delegate that returns a Task, which means that your anonymous method (async res => { ... }) has a void return type.

That means your anonymous method returns as soon as the network request in GetAll() is sent. Because ForEach() isn't waiting for it, it moves on to the next iteration. By the time ForEach() is done everything, all it has done is sent the requests, but not waited for them. So by the time you get to your return statement, you can't be sure anything has been done.

Replace that .ForEach() call with just a regular foreach so that you aren't doing that work inside a void delegate. Then you should see it behave more like you expect. Even better, use Johnathan's answer to start all the tasks inside the loop, then wait for them after.

Microsoft has a very well written series of articles on Asynchronous programming with async and await that I think you will benefit from reading. You can find the rest of the series in the table of contents on the left side of that page.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
0

Switch from resList.ToList().ForEach(...) to ordinary foreach:

foreach (var res in resList)
{
    switch (res.Type)
    {
        case Itemtype.None:
            res.SubItems = await _unit.repository.GetAll(y => y.ItemId(res.ItemId));
            break;
        case Itemtype.Low:
            //get from another place
            break;
    }
}

It seems that your are using List<T>.ForEach. It accepts Action<T> and since the action you pass to it is asynchronous your current code will just create resList.Count() of tasks and proceed to return statement without actually awaiting them. Simple reproducer can look like this:

class MyClass { public int i; }

var col = new[] { new MyClass { i = 1 }, new MyClass { i = 2 } };
col.ToList().ForEach(async i => { await Task.Delay(1); i.i *= 10; });
//col.ToList().ForEach(i => { i.i *= 10; });

Console.WriteLine(string.Join(" ", col.Select(mc => mc.i))); // will print "1 2"
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • I'm thinking async should be in front of foreach – Mike Cheel Jun 09 '20 at 21:23
  • 1
    @MikeCheel it seems that OP is using `List.ForEach` which returns `void` . – Guru Stron Jun 09 '20 at 21:40
  • i meant in front of your foreach. async in for loops can cause issues (in pre- 7.3(?) I think) c# and so i typically use Task.WaitAll (or similar). async foreach solves this problem (in c# 8 It hink)? https://medium.com/@t.masonbarneydev/iterating-asynchronously-how-to-use-async-await-with-foreach-in-c-d7e6d21f89fa – Mike Cheel Jun 09 '20 at 21:41
  • @MikeCheel why do you think that `resList` is `IAsyncEnumerable`? There is no awaiting in `resList.Count()` call, for example. – Guru Stron Jun 09 '20 at 21:45
  • @MikeCheel the article you've linked is not about issues it is about possible performance gains, which is up to debate because heavily will be dependent on scenario. – Guru Stron Jun 09 '20 at 21:48
  • You're right I don't know why I was thinking that. End of day I guess. – Mike Cheel Jun 09 '20 at 22:02