1

I am trying to await an asynchronous operation inside a select statement which is inside another select statement.

var result = someList
    .Select(table => new Table
    {
        Columns = table.Select(async column => new Column
        {
            Constraints = await GetColumnConstraints()
        })
    })
    .ToList();

The "problem" here is that the nested select statement returns a list of tasks. Normally we would use Task.WhenAll() to await all of the tasks. So the easiest thing to do would be to change the type of Columns from List<Column> to List<Task<Column>>, then use SelectMany() to fetch every Task and await them. But I cannot change the datatype of Columns.

How can I achieve such thing? I could not find any solution which doesnt involve Task.WhenAll()

Peter Bons
  • 26,826
  • 4
  • 50
  • 74
Marco Siffert
  • 535
  • 1
  • 6
  • 22

2 Answers2

2

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);
JSteward
  • 6,833
  • 2
  • 21
  • 30
1

Apparently Somelist is a sequence of Tables, where every Table has a sequence of Columns.

It seems to me that the columns of these tables do not influence GetColumnConstraints(). Why are you calling this function once per column? Wouldn't it be more efficient if you would call it only once?

var columnConstraints = await GetColumnConstraintsAsync();
var result = tables.Select(table => new Table
{
    Columns = table.Select(column => new Column
    {
        Constraints = columnConstraints,
        ...
    })
})
.ToList();

it could be that you oversimplified your problem and that the table and column being enumerated do influence the fetched constraints, for instance because they are used as input variables. Maybe, if you call the function for a 2nd time you get different constraints?

If you really need to get the constraints per column per table, then first await getting the constraints before you create your table:

var result = tables.Select(table => 
{
    var columnTasks = table.Select(column => GetColumnContraintsAsync(...)).ToArray();
    // all tasks are running now, wait until all are completed:
    await Task.WhenAll(columnTasks);
    // fetch the result from every task:
    var columnTaskResults = columnTasks.Select(columnTask => columnTask.Result).ToList();

    // create a Table with these results and return it:
    return new Table
    {
        Columns = columnTaskResults,
    };
})
.ToList();
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • 1
    I don't think there's a need for `columnTasks.Select` as `await Task.WhenAll(columnTasks)` already returns a collection of `Column` – JSteward Jan 02 '19 at 15:31
  • 2
    `Task.WhenAll` will return a collection of the results if the results are share the same type. [Docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenall?view=netframework-4.7.2) and the signature `public static System.Threading.Tasks.Task WhenAll (System.Collections.Generic.IEnumerable> tasks);` – JSteward Jan 02 '19 at 15:41
  • According to your link the return value of Task.WhenAll is: "A task that represents the completion of all of the supplied tasks." The examples also show that when this task ran to completion the Result of all original tasks is fetched to access the return value of the functions – Harald Coppoolse Jan 03 '19 at 07:23