41
votes

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?

1
Should work, try not specifying the type parameters? - It'sNotALie.
@ofstream: No, it shouldn't work. The error message is pretty specific about this. - Daniel Hilgarth
If i do not specify type parameters (remoteItems.Select(async r => await MapToLocalObject(r)).ToList())I get a List<Task<L>>> which is not what i want. - PKeno
Ah, there we go. You have to return the task, else it isn't an async method. You'll need to do two selects, one where you select the mapping, and then you select the task.result, and then tolist. That should get you a List<L>. - It'sNotALie.
@AlexFilipovici That would just do a blocking wait on the task, making the method not async. - Servy

1 Answers

74
votes

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;
  ...
}