What I want to know is how far down the rabbit hole should all be labeled async to just work? Of course, there will come a time when something must be done synchronously
No, there should not be a point where something works synchronously, and that is what everything is asynchronous about. The phrase "async all the way" actually means all the way up the call stack.
When you process a message asynchronously, you allow your requests to process the loop requests while the truly asynchronous method is executed, because when you go deep into the hole for rabit, There is no thread .
For example, when you have an async click event handler:
private async void Button_Click(object sender, RoutedEventArgs e) { await DoWorkAsync();
When the button is pressed, it is executed synchronously until the first await is pressed. Once pressed, the method returns control to the caller, which means that the button's event handler will free the user interface thread, which will free the message loop to process more requests at the same time.
The same goes for using HttpClient . For example, when you have:
public async Task<IHttpActionResult> Post(string value) { var results = await PerformWebRequests(); // Do something else here... } private async Task<IEnumerable<string>> PerformWebRequests() { var result1 = await PerformWebRequestAsync("service1/api/foo"); var result = await PerformWebRequestAsync("service2/api/foo"); return new string[] { result1, result2 }; } private async string PerformWebRequestAsync(string api) { using (HttpClient client = new HttpClient()) { await client.GetAsync(api); } // More work.. }
See how the async got to the main POST request processing method. Thus, while the asynchronous HTTP request is being processed by the network device driver, your thread returns to ASP.NET ThreadPool and can process more requests in the meantime.
The console application is a special case, because when the Main method terminates, if you do not spin a new foreground thread, the application will terminate. There you must make sure that if the only call is an asynchronous call, you will have to explicitly use Task.Wait or Task.Result . But in this case, by default, SynchronizationContext is ThreadPoolSynchronizationContext , where there is no way to cause a deadlock.
In conclusion, asynchronous methods should not be processed synchronously at the top of the stack , if there is no exotic use case (for example, a console application), they should be passed asynchronously completely allowing the thread to be freed whenever possible.