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();
General approach: 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.