How to hydrate a Dictionary with the results of async calls?

13,579

Solution 1

If you insist on doing it with linq, Task.WhenAll is the key to "hydrate" the dictionary:

int[] numbers = new int[] { 1, 2 , 3};

KeyValuePair<int, string>[] keyValArray = //using KeyValuePair<,> to avoid GC pressure
    await Task.WhenAll(numbers.Select(async p => 
        new KeyValuePair<int, string>(p, await DoSomethingReturnString(p))));

Dictionary<int, string> dict = keyValArray.ToDictionary(p => p.Key, p => p.Value);

Solution 2

LINQ methods do not support asynchronous actions (e.g., asynchronous value selectors), but you can create one yourself. Here is a reusable ToDictionaryAsync extension method that supports an asynchronous value selector:

public static class ExtensionMethods
{
    public static async Task<Dictionary<TKey, TValue>> ToDictionaryAsync<TInput, TKey, TValue>(
        this IEnumerable<TInput> enumerable,
        Func<TInput, TKey> syncKeySelector,
        Func<TInput, Task<TValue>> asyncValueSelector)
    {
        Dictionary<TKey,TValue> dictionary = new Dictionary<TKey, TValue>();

        foreach (var item in enumerable)
        {
            var key = syncKeySelector(item);

            var value = await asyncValueSelector(item);

            dictionary.Add(key,value);
        }

        return dictionary;
    }
}

You can use it like this:

private static async Task<Dictionary<int,string>>  DoIt()
{
    int[] numbers = new int[] { 1, 2, 3 };

    return await numbers.ToDictionaryAsync(
        x => x,
        x => DoSomethingReturnString(x));
}

Solution 3

If calling from an asynchronous method, you can write a wrapper method that creates a new dictionary and builds a dictionary by iterating over each number, calling your DoSomethingReturnString in turn:

public async Task CallerAsync()
{
    int[] numbers = new int[] { 1, 2, 3 };
    Dictionary<int, string> dictionary = await ConvertToDictionaryAsync(numbers);
}

public async Task<Dictionary<int, string>> ConvertToDictionaryAsync(int[] numbers)
{
    var dict = new Dictionary<int, string>();

    for (int i = 0; i < numbers.Length; i++)
    {
        var n = numbers[i];
        dict[n] = await DoSomethingReturnString(n);
    }

    return dict;
}

Solution 4

This is just a combination of @Yacoub's and @David's answers for an extension method which uses Task.WhenAll

public static async Task<Dictionary<TKey, TValue>> ToDictionaryAsync<TInput, TKey, TValue>(
    this IEnumerable<TInput> enumerable,
    Func<TInput, TKey> syncKeySelector,
    Func<TInput, Task<TValue>> asyncValueSelector)
{
    KeyValuePair<TKey, TValue>[] keyValuePairs = await Task.WhenAll(
        enumerable.Select(async input => new KeyValuePair<TKey, TValue>(syncKeySelector(input), await asyncValueSelector(input)))
    );
    return keyValuePairs.ToDictionary(pair => pair.Key, pair => pair.Value);
}
Share:
13,579

Related videos on Youtube

Vivian River
Author by

Vivian River

Updated on October 17, 2022

Comments

  • Vivian River
    Vivian River over 1 year

    Suppose I have code that looks like this:

    public async Task<string> DoSomethingReturnString(int n) { ... }
    int[] numbers = new int[] { 1, 2 , 3};
    

    Suppose that I want to create a dictionary that contains the result of calling DoSomethingReturnString for each number similar to this:

    Dictionary<int, string> dictionary = numbers.ToDictionary(n => n,
        n => DoSomethingReturnString(n));
    

    That won't work because DoSomethingReturnString returns Task<string> rather than string. The intellisense suggested that I try specifying my lambda expression to be async, but this didn't seem to fix the problem either.

    • Matt Burland
      Matt Burland almost 8 years
      DoSomethingReturnString(n).Result, but then it's blocking. If that's not what you are going for then you'd need async function that returns a Task<Dictionary<int,string>>
    • David L
      David L almost 8 years
      This is a great example of why "async all the way down" is a guiding principle when working with async code.
  • David L
    David L almost 8 years
    In addition, a LINQ approach may also end up iterating twice depending on usage and structure...once to select and another time to create the dictionary. This approach will always only iterate once.
  • pasx
    pasx about 4 years
    With C#7 you can use Tuples instead of KeyValuePair: var keyValTuples = await Task.WhenAll(numbers.Select(async p => (p, await DoSomethingReturnString(p))));
  • Theodor Zoulias
    Theodor Zoulias over 2 years
    Instead of using ToDictionary, you could also use the constructor: return new Dictionary<TKey, TValue>(keyValuePairs); It might be slightly more efficient.