Given an extensible interface, how should one decide whether to define synchronous or asynchronous methods? (Note that in this case I mean interface in the general software engineering sense, so feel free to substitute base class, exported DLL function, function pointer/delegate, etc. as desired.)
There are no hard and fast rules beyond the obvious — primarily I/O-bound interactions (e.g. network or file access) should be async. Things get more hazy in the subtle cases where sometimes I/O is involved, but not always.
Imagine a web testing library which provides an extensible
ResponseValidator class. A user could override the
Validate(HttpWebResponse) method to implement a custom validation task. Most use cases would involve static assertions, for instance to ensure the header contains a properly formatted ETag. But what if you needed to send the response text to an online spelling API? Technically you’d want to use async to manage the additional web request, but the interface here does not support this. Given that there is an acceptable alternative (e.g. using a locally installed spell check library) it would not be worth forcing an async signature for something which would rarely, if ever, be needed.
Now consider a system with a plug-in model that offers varying degrees of isolation such that the host could potentially be in a separate process. To provide a unified programming model, it would possible to hide all the details and allow clients to interact with plug-ins as if they were local objects (think WCF proxies or COM servers). This approach sounds quite convenient but happens to run afoul of Martin Fowler’s First Law of Distributed Objects: “don’t distribute your objects.” As Fowler points out, there are too many encapsulation-breaking realities to make this a recommended approach; processes can crash, communication channels can break, and everything is far slower than a truly local method call. If you must do this, consider a compromise similar to WCF’s client-side async contracts: define a synchronous interface for the plug-in implementer but a fully async interface for the plug-in client.
What about a case where I/O is certainly involved but a “defective” implementation forces synchronous behavior? A real-life example might be a system that interacts with multiple work item trackers — TFS, JIRA, Trello, AgileZen, etc. All items in that list expose a REST API except TFS, whose commonly exposed object model is simple but fully synchronous (though there is an OData service beta release). Should the lone sync API binding force you into a totally sync implementation for all? Probably not. You can begrudgingly use
Task.Run to “fix” the blocking calls until a better API emerges. Otherwise you risk causing a huge ripple on the order of a complete redesign if you later need to go full async.
Is it ever acceptable to provide both async and sync methods? This might make sense in “legacy” libraries, especially those that were built in the
IAsyncResult days before async was an accessible and familiar practice. Think the Stream API in .NET, which arrived back in .NET 1.0. To avoid confusion and promote better practices, modern apps should not provide synchronous APIs for I/O-centric operations.