Fill c# Dictionary asynchronously
async-await, c#, for-loop, linq, resharper
Solution
Actually the generic method can be rather interesting so I'll post a solution. Your initial implementation has a performance issue you could solve if you wished, and your application domain allows it.
If you want to add items to the dictionary asynchronously then it is probably desirable to allow those async items to run in parallel. You want to start with a method that accepts an enumerable of keys and values. The values should be functions which generate tasks, not tasks. This is so you don't have all the tasks running at once.
You then use a semaphore to control how many tasks are started at the same time. There is also a fallback function below for when you are happy for all tasks to start at the same time.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace _43909210
{
public static class EnumerableExtensions
{
private static async Task<Dictionary<TKey, TValue>> ToDictionary<TKey,TValue>
(this IEnumerable<(TKey key, Func<Task<TValue>> valueTask)> source)
{
var results = await Task.WhenAll
( source.Select( async e => ( key: e.key, value:await e.valueTask() ) ) );
return results.ToDictionary(v=>v.key, v=>v.value);
}
public class ActionDisposable : IDisposable
{
private readonly Action _Action;
public ActionDisposable(Action action) => _Action = action;
public void Dispose() => _Action();
}
public static async Task<IDisposable> Enter(this SemaphoreSlim s)
{
await s.WaitAsync();
return new ActionDisposable( () => s.Release() );
}
/// <summary>
/// Generate a dictionary asynchronously with up to 'n' tasks running in parallel.
/// If n = 0 then there is no limit set and all tasks are started together.
/// </summary>
/// <returns></returns>
public static async Task<Dictionary<TKey, TValue>> ToDictionaryParallel<TKey,TValue>
( this IEnumerable<(TKey key, Func<Task<TValue>> valueTaskFactory)> source
, int n = 0
)
{
// Delegate to the non limiting case where
if (n <= 0)
return await ToDictionary( source );
// Set up the parallel limiting semaphore
using (var pLimit = new SemaphoreSlim( n ))
{
var dict = new Dictionary<TKey, TValue>();
// Local function to start the task and
// block (asynchronously ) when too many
// tasks are running
async Task
Run((TKey key, Func<Task<TValue>> valueTask) o)
{
// async block if the parallel limit is reached
using (await pLimit.Enter())
{
dict.Add(o.key, await o.valueTask());
}
}
// Proceed to start the tasks
foreach (var task in source.Select( Run ))
await task;
// Wait for all tasks to finish
await pLimit.WaitAsync();
return dict;
}
}
}
}
Problem
I am using this code below that loops through a list of Contracts and gets a list of Rates for each Contract. Then it stores the result in a Dictionary. ``` /// <summary> /// Get the Rates for the Job keyed to the id of the Organisation /// </summary> async Task<Dictionary<int, IReadOnlyList<Rate>>> GetRatesAsync(int jobId) { var jobContracts = await _contractClient.GetJobContractsByJobAsync(jobId); var result = new Dictionary<int, IReadOnlyList<Rate>>(); foreach (var jobContract in jobContracts) { result.Add(jobContract.Contract.Organisation.Id, await _contractClient.GetContractRatesByContractAsync(jobContract.Contract.Id)); } return result; } ``` Resharper helpfully advises me that "Loop can be converted into LINQ-expression". However the auto-generated fix by Resharper (below) does not compile. ``` return jobContracts.ToDictionary(jobContract => jobContract.Contract.Organisation.Id, jobContract => await _contractClient.GetContractRatesByContractAsync(jobContract.Contract.Id)); ``` and then The error is `The await operator can only be used within an async lamba expression`. So chucking an async in and changing it to below: ``` return jobContracts.ToDictionary(jobContract => jobContract.Contract.Organisation.Id, async jobContract => await _contractClient.GetContractRatesByContractAsync(jobContract.Contract.Id)); ``` and then the error is now `Cannot implicitly convert type 'System.Collections.Generic.Dictionary<int, System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Rate>>>' to 'System.Collections.Generic.Dictionary<int, System.Collections.Generic.IReadOnlyList<Rate>>'` and it wants me to change the signature to: ``` async Task<Dictionary<int, Task<IReadOnlyList<Rate>>>> GetContractRatesAsync() ``` and then This last change now compiles, but all my calling methods are expecting a Task of `Dictionary<int, IReadOnlyList<Rate>>` to await, not a `Dictionary<int, Task<IReadOnlyList<Rate>>`. Is there some way of awaiting this to convert it to a `Dictionary<int, IReadOnlyList<Rate>>`? Or some other way to get the data out asynchronously?