Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using IEnumerable.Aggregate with asynchronous calls

I'm trying to use the LINQ IEnumerable.Aggregate function to create a string consisting of files retrieved through async calls. Not a hundred percent sure that it's possible, and I'm also aware that there are other solutions, but I'd like to give it a try.

For now my code looks like this:

private static async Task<string> GetFiles(IEnumerable<string> filePaths)
{
    return filePaths.Aggregate(async (current, path) => current + await GetFile(path));
}

But the "async" inside the method call is error marked saying "the return of an async method must be void, Task, or Task". I get that error in general, but I'm not sure how to arrange this specific case to avoid it. Any ideas?

UPDATE: Just to clarify, the GetFile() method is indeed asynchronous and returns Task<string>:

private static async Task<string> GetFile(string filePath) { ... }

No need to get into the specific code, but for those interested it uses HttpClient.GetAsync(filePath) and the returns its response.Content.ReadAsStringAsync().Result.

like image 666
Don Simon Avatar asked Feb 18 '15 13:02

Don Simon


3 Answers

Aggregate method won't work asynchronously. It doesn't support Task based delegates. You need to create a result sequence yourself by awaiting it in prior to call Aggregate method.

Something like this should work:

private static async Task<string> GetFiles(IEnumerable<string> filePaths)
{
    var files = filePaths
        .Select(p => GetFile(p))
        .ToArray();
    var results = await Task.WhenAll(files);

    return results
        .Aggregate((current, path) => current + path);
}
like image 65
Sriram Sakthivel Avatar answered Nov 09 '22 10:11

Sriram Sakthivel


As @Sriram said, LINQ and async-await don't work that well together because there's no built-in support for async Task delegates.

What you can do is create an async overload of aggregate yourself:

public static class AsynchronousEnumerable
{
    public static async Task<TSource> AggregateAsync<TSource>
                                      (this IEnumerable<TSource> source,
                                       Func<TSource, TSource, Task<TSource>> func)
    {
       using (IEnumerator<TSource> e = source.GetEnumerator())
       {
            if (!e.MoveNext())
            {
                throw new InvalidOperationException("Sequence contains no elements");
            }

            TSource result = e.Current;
            while (e.MoveNext()) result = await func(result, e.Current);
            return result;
        }
    }
}

And now you can do the following:

private static Task<string> GetFiles(IEnumerable<string> filePaths)
{
    return filePaths.AggregateAsync(async (current, path) => current + 
                                                             await GetFile(path));
}
like image 21
Yuval Itzchakov Avatar answered Nov 09 '22 10:11

Yuval Itzchakov


If you want to use async inside Aggregate, you must realize that anything asynchronous always returns a Task. Taking this into account, it becomes obvious that the result of your Aggregate call should therefore also be a Task.

For example, calculating the sum of a collection of numbers that are returned asynchronously:

private static async Task<int> GetSumAsync(IEnumerable<Task<int>> numbers) {
  return await numbers
    .Aggregate(Task.FromResult(0), async (sumSoFar, nextNumber) => (await sumSoFar) + (await nextNumber));
}

I am a little confused as to what you're exactly hoping to do with your GetFiles method. You do realize that Aggregate reduces a collection to just one thing, right? (The 'sum' function is a good example)

like image 30
Moeri Avatar answered Nov 09 '22 10:11

Moeri