Well, first you want a Task
that represents all the operations. The simplest way to do this is with a bit of LINQ:
Task.WhenAll(documents.Select(i => records.BulkWriteAsync(...)));
Then, you ideally want to await
that task. If that isn't possible, you can try
task.GetAwaiter().GetResult();
However, make sure that none of the tasks have thread affinity - that's a great way to get a deadlock. Waiting for a task on the UI thread while the task itself needs the UI thread is a typical example.
The whole point of await
is that it allows you to handle asynchronous code as if it were synchronous. So from the outside, it appears as if you never left the method until you actually get to a return
(or the end of the method). For this to work, however, your method must return a Task
(or Task<T>
), and the callee must await
your method in turn.
So a code like this:
try
{
tasks = Task.WhenAll(documents.Select(i => ...));
await tasks;
}
catch (Exception ex)
{
// Handle the exception
}
will appear to run completely synchronously, and all exceptions will be handled as usual (though since we're using Task.WhenAll
, some will be wrapped in AggregateException
).
However, this isn't actually possible to handle with the way .NET and C# is built, so the C# compiler cheats - await
is basically a return
that gives you a promise of the result you'll get in the future. And when that happens, the control returns back to where the await
left the last time. Task
is that promise - if you use async void
, there's no way for the callee to know what's happening, and it has no option but to continue as if the asynchronous method was a run-and-forget method. If you use async Task
, you can await
the async method and everything "feels" synchronous again. Don't break the chain, and the illusion is perfect :)