Having already been caught out by the ASP.NET deadlock issue when using async code synchronously
I did a lot of reading on the subject, from various sources and ended up compiling a list of rules/conventions to follow when dealing with async code.
This is by no means a gospel list and I don't elaborate on why each guideline exists, but it's just a concise set of reminders of things to consider when writing async code, since the compiler will not pick up warnings for a lot of these things.
The list is shared below:
- Don't use .Result or .Wait() - use async/await (unless absolutely necessary - e.g. destructor, domain value factory - see final point)
- Don't used async void
- Always use "ConfigureAwait(false)" inside async library code (unless you need the context)
- Don't "await Task.FromResult" - remove the async and just return the Task directly (unless async null or type casting).
- When returning Task, don't return "null" - you must return Task.FromResult<T>(null);
- Make sure any function returning Task is suffixed as "Async" to guide the caller
- Don't use parallel for each, use tasks and Task.WhenAll (unless need custom partitioning)
- Constructors and destructors cannot be marked async, therefore should not call async methods (no compiler warning)
- For ctors - consider making it a factory with CreateInstanceAsync
- For destructors - try to design against it, or fall back to Task.Run().GetAwaiter().GetResult() syntax (see final point)
- When using .Wait() and .Result syntax, ensure it runs on a background thread with no context e.g.
... Task.Run(() => theMainAsync()).GetAwaiter().GetResult();
In general, start off by making the interface return Task<> .. update the classes to return Task, then look to see if anything inside the function is truly async. If yes - mark the function async and use await (unless you can pass through).. if No, then just return a Task.FromResult to satisfy the interface..
Don't use async/await inside of a parallel loop. You can block here since its already async and on another thread, or change to tasks.
If you *need* to convert to synchronous (such as for passing a value factory) then use Task.Run to encapsulate the blocking call, so that its on another thread with a different context and use .GetAwaiter().GetResult() to unwrap exceptions.