C# – Aren’t the guidelines of async/await usage in C# contradicting the concepts of good architecture and abstraction layering

Architectureasyncc

This question concerns the C# language, but I expect it to cover other languages such as Java or TypeScript.

Microsoft recommends best practices on using asynchronous calls in .NET. Among these recommendations, let's pick two:

  • change the signature of the async methods so that they return Task or Task<> (in TypeScript, that'd be a Promise<>)
  • change the names of the async methods to end with xxxAsync()

Now, when replacing a low-level, synchronous component by an async one, this impacts the full stack of the application. Since async/await has a positive impact only if used "all the way up", it means the signature and method names of every layer in the application must be changed.

A good architecture often involves placing abstractions between each layers, such that replacing low-level components by others is unseen by the upper-level components. In C#, abstractions take the form of interfaces. If we introduce a new, low-level, async component, each interface in the call stack needs to be either modified or replaced by a new interface. The way a problem is solved (async or sync) in an implementing class is not hidden (abstracted) to the callers anymore. The callers have to know if it's sync or async.

Aren't async/await best practices contradicting with "good architecture" principles?

Does it mean that each interface (say IEnumerable, IDataAccessLayer) needs their async counterpart (IAsyncEnumerable, IAsyncDataAccessLayer) such that they can be replaced in the stack when switching to async dependencies?

If we push the problem a little further, wouldn't it be simpler to assume every method to be async (to return a Task<> or Promise<>), and for the methods to synchronize the async calls when they're not actually async? Is this something to be expected from the future programming languages?

Best Answer

What Color Is Your Function?

You may be interested in Bob Nystrom's What Color Is Your Function1.

In this article, he describes a fictional language where:

  • Each function has a color: blue or red.
  • A red function may call either blue or red functions, no issue.
  • A blue function may only call blue functions.

While fictitious, this happens quite regularly in programming languages:

  • In C++, a "const" method may only call other "const" methods on this.
  • In Haskell, a non-IO function may only call non-IO functions.
  • In C#, a sync function may only call sync functions2.

As you have realized, because of these rules, red functions tend to spread around the code base. You insert one, and little by little it colonizes the whole code base.

1 Bob Nystrom, apart from blogging, is also part of the Dart team and has written this little Crafting Interpreters serie; highly recommended for any programming language/compiler afficionado.

2 Not quite true, as you may call an async function and block until it returns, but...

Language Limitation

This is, essentially, a language/run-time limitation.

Language with M:N threading, for example, such as Erlang and Go, do not have async functions: each function is potentially async and its "fiber" will simply be suspended, swapped out, and swapped back in when it's ready again.

C# went with a 1:1 threading model, and therefore decided to surface synchronicity in the language to avoid accidentally blocking threads.

In the presence of language limitations, coding guidelines have to adapt.

Related Topic