Async Await in Linq Select

Async await in linq select

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
.Select(t => t.Result)
.Where(i => i != null)
.ToList();

But this seems very weird to me, first of all the use of async and await in the select. According to this answer by Stephen Cleary I should be able to drop those.

The call to Select is valid. These two lines are essentially identical:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(There's a minor difference regarding how a synchronous exception would be thrown from ProcessEventAsync, but in the context of this code it doesn't matter at all.)

Then the second Select which selects the result. Doesn't this mean the task isn't async at all and is performed synchronously (so much effort for nothing), or will the task be performed asynchronously and when it's done the rest of the query is executed?

It means that the query is blocking. So it is not really asynchronous.

Breaking it down:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

will first start an asynchronous operation for each event. Then this line:

                   .Select(t => t.Result)

will wait for those operations to complete one at a time (first it waits for the first event's operation, then the next, then the next, etc).

This is the part I don't care for, because it blocks and also would wrap any exceptions in AggregateException.

and is it completely the same like this?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
.Where(result => result != null).ToList();

Yes, those two examples are equivalent. They both start all asynchronous operations (events.Select(...)), then asynchronously wait for all the operations to complete in any order (await Task.WhenAll(...)), then proceed with the rest of the work (Where...).

Both of these examples are different from the original code. The original code is blocking and will wrap exceptions in AggregateException.

Using async/await inside a Select linq query

So at the end of your method, customerTasks contains an IEnumerable<Task> that has not been enumerated. None of the code within the Select even runs.

When creating tasks like this, it's probably safer to materialize your sequence immediately to mitigate the risk of double enumeration (and creating a second batch of tasks by accident). You can do this by calling ToList on your sequence.

So:

var customerTasks = ids.Select(async i =>
{
ICustomerRepo repo = new CustomerRepo();
var id = await repo.getCustomer(i); //consider changing to GetCustomerAsync
Console.WriteLine(id);

}).ToList();

Now... what to do with your list of tasks? You need to wait for them all to complete...

You can do this with Task.WhenAll:

await Task.WhenAll(customerTasks);

You could take this a step further by actually returning a value from your async delegate in the Select statement, so you end up with an IEnumerable<Task<Customer>>.

Then you can use a different overload of Task.WhenAll:

IEnumerable<Task<Customer>> customerTasks = ids.Select(async i =>
{
ICustomerRepo repo = new CustomerRepo();
var c = await repo.getCustomer(i); //consider changing to GetCustomerAsync
return c;

}).ToList();

Customer[] customers = await Task.WhenAll(customerTasks); //look... all the customers

Of course, there are probably more efficient means of getting several customers in one go, but that would be for a different question.

If instead, you'd like to perform your async tasks in sequence then:

var customerTasks = ids.Select(async i =>
{
ICustomerRepo repo = new CustomerRepo();
var id = await repo.getCustomer(i); //consider changing to GetCustomerAsync
Console.WriteLine(id);

});
foreach(var task in customerTasks) //items in sequence will be materialized one-by-one
{
await task;
}

Using Async/Await inside nested LINQ-Select without having to use Task.WhenAll

You don't need to change the type of Column you just need to await the results outside of the initializer for your table. That allows you to collect the results in columnTasks, await all of those and then create your new table.

var result = someList.Select(async table =>
{
var columnTasks = table.Select(async column => new Column()
{
Constraints = await GetColumnConstraints()
});
var columns = await Task.WhenAll(columnTasks);
return new Table()
{
Columns = columns
};
});

Notice though that async works its way up the chain so reuslt is now IEnumerable<Task<Table>> and you'll have to await Task.WhenAll that to get your final collection of Table

var tables = await Task.WhenAll(result);

LINQ Select analogue for async method

Looks like there is no other solution currently (until C# 8.0) except enumerating it manually. I made an extension method for that (returns a List instead of Array as it's simpler):

public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source)
{
var result = new List<T>();
foreach (var item in source)
{
result.Add(await item);
}
return result;
}


Related Topics



Leave a reply



Submit