4

I want to use ToDictionary on a bunch of Task<MyObject> (see below). This works ok if I use TheTask.Result, but not await

This works:

Dictionary<CarId, Task<Model>> allModelTasks = carIds.ToDictionary(cId =>cId, cId=> GetModelForCar(cId));                                  
await Task.WhenAll(allModelTasks.Values);
Dictionary<CarId, Model> allModels = allModelTasks.ToDictionary(mt => mt.Key, mt => mt.Value.Result);

But if I replace last row with

Dictionary<CarId, Model> allModels = allModelTasks.ToDictionary(mt => mt.Key, async mt => await mt.Value);

I get an error message that says "cant convert from Dict<CarId, Task<Model>> to Dict<CarId, Model>". As I see it, the rows should be equivalent.

(The recommendation from here, seems to be to use await instead of .Result, even after EDIT Task.WhenAll)

Cowborg
  • 2,645
  • 3
  • 34
  • 51
  • 2
    Please pay attention to edits - my edit made generics visible with the correct syntax. Yours wiped that out and made them invisible in the first para, for instance. – Damien_The_Unbeliever May 20 '20 at 08:37
  • How does `GetModelForCar` look like? – Pavel Anikhouski May 20 '20 at 08:41
  • 1
    @PavelAnikhouski Given that the first code works, `Task GetModelForCar(CarId id)`. – GSerg May 20 '20 at 08:44
  • Take a look at this question: [How to hydrate a Dictionary with the results of async calls?](https://stackoverflow.com/questions/37796139/how-to-hydrate-a-dictionary-with-the-results-of-async-calls) It contains implementations of a `ToDictionaryAsync` method. These implementations `await` each value sequentially though, so it's not what you want. Which indicates why there is no such built-in method: it can be implemented in multiple ways with different behaviors. – Theodor Zoulias May 20 '20 at 09:26
  • "even after Task.WaitAll" - the advice is also: to avoid `WaitAll` :) – Marc Gravell May 20 '20 at 09:48
  • Ok... just went away for a few hours and had 7 answers and comments... Ill try to answer all. @ Pavel:see answer from CSerg. @ Theo: Thanks! It seems like what I want! @ Marc: Im not using WaitAll, I just happen to miswrite (for the 30th time :D), I meant WhenAll – Cowborg May 20 '20 at 11:59

1 Answers1

8

You seem to be ignoring that to be able to write await in that last line, you also had to add another async. Which means the await isn't "in" your current method - it's inside a new async lambda that, of course, being async returns a Task.

You can't use await inside a lambda here and expect not to still have a task to unwrap. Your first code is fine, and doesn't suffer any of the potential problems that accessing .Result can cause because the Tasks are all known to already be complete.


You could create or copy some form of ToDictionaryAsync extension method into your class, something like:

public static class TaskResultExtensions
{
    public static async Task<Dictionary<TKey, TValue>> ToDictionaryAsync<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, Task<TValue>>> source)
    {
        var dict = new Dictionary<TKey, TValue>();
        foreach (var kvp in source)
        {
            dict.Add(kvp.Key, await kvp.Value);
        }

        return dict;
    }
}

Which would then allow your final line to be

Dictionary<CarId, Model> allModels = await allModelTasks.ToDictionaryAsync();

instead.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Ok, yes. I get that .Result is ok bec of WhenAll(), but I wanted it to work with await, bec. (1) In my current project, my code might be copy-pasted and dont want to encurage .Result for blocking reaseon. (2) I might not be 100% sure What Stephen Cleary means in anwer (" I recommend using await because it's clearly correct, while Result can cause problems in other scenarios."). If those scenarios was without any await/WhenAll(). ... but mostly out of curiousity. To get to my point: Where am I supposed to put that *await*? Could you correct that last line for me? – Cowborg May 20 '20 at 12:05
  • @Cowborg - if there was a straightforward place to put an `await` in that last line, I'd have suggested it. If you really want to `await` then you can create a new Dictionary yourself and then iterate the contents of your first dictionary awaiting each value in turn before adding to the new dictionary. But I find that a "noisier" implementation. – Damien_The_Unbeliever May 20 '20 at 12:34