46

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?

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
PKeno
  • 2,694
  • 7
  • 20
  • 37
  • Should work, try not specifying the type parameters? – It'sNotALie. Feb 18 '13 at 14:25
  • 1
    @ofstream: No, it shouldn't work. The error message is pretty specific about this. – Daniel Hilgarth Feb 18 '13 at 14:29
  • If i do not specify type parameters (`remoteItems.Select(async r => await MapToLocalObject(r)).ToList()`)I get a `List>>` which is not what i want. – PKeno Feb 18 '13 at 14:29
  • 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. – It'sNotALie. Feb 18 '13 at 14:31
  • I supose you need to use Task.WaitAll if you need to complete all task in parallel. And then collect the results of Tasks – gabba Feb 18 '13 at 14:36
  • Culdn't you use `remoteItems.Select(r =>{return MapToLocalObject(r).GetAwaiter().GetResult();}).ToList();`? – Alex Filipovici Feb 18 '13 at 14:49
  • 3
    @AlexFilipovici That would just do a blocking wait on the task, making the method not async. – Servy Feb 18 '13 at 15:03

1 Answers1

82

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;
  ...
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I'd be interested to know what the effect of adding ".ToArray()" to the end of remoteItems.Select(remoteItem => MapToLocalObject(remoteItem)) would be. Wouldn't it enable me to access the results of tasks that have completed, if the outer task is cancelled? Sorry if this is not clear! – Paul Suart May 20 '13 at 02:04
  • 2
    If you reify the sequence of tasks into a collection, you would be able to access the results of those tasks, e.g., to build a partial result in the case of cancellation. – Stephen Cleary May 20 '13 at 02:23