Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling async method in IEnumerable.Select

Tags:

c#

async-await

I have the following code, converting items between the types R and L using an async method:

class MyClass<R,L> {

    public async Task<bool> MyMethodAsync(List<R> remoteItems) {
        ...

        List<L> mappedItems = new List<L>();
        foreach (var remoteItem  in remoteItems )
        {
            mappedItems.Add(await MapToLocalObject(remoteItem));
        }

        //Do stuff with mapped items

        ...
    }

    private async Task<L> MapToLocalObject(R remoteObject);
}

Is this possible to write using an IEnumerable.Select call (or similar) to reduce lines of code? I tried this:

class MyClass<R,L> {

    public async Task<bool> MyMethodAsync(List<R> remoteItems) {
        ...

        List<L> mappedItems = remoteItems.Select<R, L>(async r => await MapToLocalObject(r)).ToList<L>();

        //Do stuff with mapped items

        ...
    }
}

But i get error:

"Cannot convert async lambda expression to delegate type 'System.Func<R,int,L>'. An async lambda expression may return void, Task or Task<T>, none of which are convertible to 'System.Func<R,int,L>'."

I believe i am missing something about the async/await keywords, but i cannot figure out what. Does any body know how i can modify my code to make it work?

like image 625
PKeno Avatar asked Feb 18 '13 14:02

PKeno


1 Answers

You can work this out by considering the types in play. For example, MapToLocalObject - when viewed as an asynchronous function - does map from R to L. But if you view it as a synchronous function, it maps from R to Task<L>.

Task is a "future", so Task<L> can be thought of as a type that will produce an L at some point in the future.

So you can easily convert from a sequence of R to a sequence of Task<L>:

IEnumerable<Task<L>> mappingTasks = remoteItems.Select(remoteItem => MapToLocalObject(remoteItem));

Note that there is an important semantic difference between this and your original code. Your original code waits for each object to be mapped before proceeding to the next object; this code will start all mappings concurrently.

Your result is a sequence of tasks - a sequence of future L results. To work with sequences of tasks, there are a few common operations. Task.WhenAll and Task.WhenAny are built-in operations for the most common requirements. If you want to wait until all mappings have completed, you can do:

L[] mappedItems = await Task.WhenAll(mappingTasks);

If you prefer to handle each item as it completes, you can use OrderByCompletion from my AsyncEx library:

Task<L>[] orderedMappingTasks = mappingTasks.OrderByCompletion();
foreach (var task in orderedMappingTasks)
{
  var mappedItem = await task;
  ...
}
like image 172
Stephen Cleary Avatar answered Nov 09 '22 23:11

Stephen Cleary