Search code examples
c#linqasync-await

How to GroupBy by result of async Task<string> method and get a grouping by string in C#?


I have the below method (it's an extension method but not relevant to this question) and I would like to use GroupBy on the results of the method.

class MyClass 
{
    public async Task<string> GetRank() 
    {
        return "X";
    }

    public async static Task Test()
    {
       List<MyClass> items = new List<MyClass>() { new MyClass() };
       var grouped = items.GroupBy(async _ => (await _.GetRank()));
    }
}

The type of grouped is IGrouping<Task<string>, MyClass>, however I need to group by the actual awaited result of the async method (string). Despite using await and making the lambda async, I still get IGrouping<Task<string>, ..> instead of IGrouping<string, ...>

How to use GroupBy and group by a result of async Task<string> method and get a grouping by string?


Solution

  • Here is an asynchronous version of GroupBy. It expects a task as the result of keySelector, and returns a task that can be awaited:

    public static async Task<IGrouping<TKey, TSource>[]> GroupByAsync<TSource, TKey>(
        this IEnumerable<TSource> source, Func<TSource, Task<TKey>> keySelector)
    {
        List<KeyValuePair<TKey, TSource>> entries = new();
        if (source.TryGetNonEnumeratedCount(out int count)) entries.Capacity = count;
        foreach (TSource item in source)
        {
            TKey key = await keySelector(item).ConfigureAwait(false);
            entries.Add(new(key, item));
        }
        return entries.GroupBy(entry => entry.Key, entry => entry.Value).ToArray();
    }
    

    It can be used like this:

    class MyClass
    {
        public async Task<string> GetRank()
        {
            await Task.Delay(100);
            return "X";
        }
    
        public async static Task Test()
        {
            var items = new List<MyClass>() { new MyClass(), new MyClass() };
            var grouped = items.GroupByAsync(async _ => (await _.GetRank()));
            foreach (var grouping in await grouped)
            {
                Console.WriteLine($"Key: {grouping.Key}, Count: {grouping.Count()}");
            }
        }
    }
    

    Output:

    Key: X, Count: 2

    The keySelector is invoked sequentially, for one item at a time. In case you are interested for a concurrent implementation, you could take a look at the 2nd revision of this answer.