How to hydrate a Dictionary with the results of async calls?
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);
}
Related videos on Youtube
Vivian River
Updated on October 17, 2022Comments
-
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 thanstring
. 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 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 aTask<Dictionary<int,string>>
-
David L almost 8 yearsThis is a great example of why "async all the way down" is a guiding principle when working with async code.
-
-
David L almost 8 yearsIn 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 about 4 yearsWith C#7 you can use Tuples instead of KeyValuePair: var keyValTuples = await Task.WhenAll(numbers.Select(async p => (p, await DoSomethingReturnString(p))));
-
Theodor Zoulias over 2 yearsInstead of using
ToDictionary
, you could also use the constructor:return new Dictionary<TKey, TValue>(keyValuePairs);
It might be slightly more efficient.