28

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.

Vivian River
  • 31,198
  • 62
  • 198
  • 313

4 Answers4

19

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);
shay__
  • 3,815
  • 17
  • 34
  • 3
    With C#7 you can use Tuples instead of KeyValuePair: var keyValTuples = await Task.WhenAll(numbers.Select(async p => (p, await DoSomethingReturnString(p)))); – pasx May 15 '20 at 07:29
9

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));
}
Yacoub Massad
  • 27,509
  • 2
  • 36
  • 62
5

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) where TKey : notnull
{
    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);
}
Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
LucidObscurity
  • 309
  • 3
  • 4
  • Instead of using `ToDictionary`, you could also use the constructor: `return new Dictionary(keyValuePairs);` It might be slightly more efficient. – Theodor Zoulias Jan 25 '22 at 09:42
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;
}
David L
  • 32,885
  • 8
  • 62
  • 93
  • Is it not possible to do this without using a loop? Using a lambda expression seems cleaner to me. – Vivian River Jun 13 '16 at 18:14
  • The lambda will iterate internally as is. The syntactic sugar of the lambda in your case is actually getting in the way of properly awaiting. It's certainly possible, but it may also hurt your readability. – David L Jun 13 '16 at 18:24
  • 1
    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. – David L Jun 13 '16 at 18:28