C# is my favorite language. It is very powerful, flexible, and modern.
You can use it with just basic knowledge of the language, and it works very well.
But when applications become more complex and big, you need to take in consideration more aspects of the language. At this point, you need to understand very well how multiple concepts work, and you start to write code in a different way, more elaborate. And here you have a revelation: once you start to write code considering these more advanced aspects, you see that the language is not so nice. But even more, you realize that the designers of the language could have take different decisions, in order to have maximum performances and still allowing to write nice code.
ConfigureAwait(true) as default
When you write asynchronous code, using async/await, the await works that after the execution of the async method, the runtime restore the execution context on the previous thread.
This is very important for all kinds of UI applications: Windows (with WinForms, WPF and UWP), iOS and Android (with MAUI and the previous Xamarin), and will be needed with Blazor (once in .NET 8 they will introduce Web Workers for multitasking work).
The point is that these use cases are just a fraction of all .NET code: we have web/API applications (where there is no any UI at all), but even in UI applications, we can run multiple tasks in background thread - even here, we just need to re-enter the main thread only when updating the UI.
So, almost every time we use an await instruction, we should also add ConfigureAwait(false).
And because this is so important, there is also a Code Analysis rule, CA2007: Do not directly await a Task.
It makes sense, so that even when you want to re-enter the UI thread, pass ConfigureAwait(true), that, even if it is the default, explicitly documents the decision to re-enter the previous context.
How to improve: we could add a project option so that ConfigureAwait(false) becomes the default. This wouldn't break existing applications, in a similar way to what has been done with nullable reference types.
Need to mark methods with async
Currently asynchronous methods, containing an await statement, must be marked with the async keyword, so that the compiler know that it is needed to create a state machine.
The issue of this decision is that if, during code evolutions, the await operation is removed, but the async keyword is still left, the compiler will still create a state machine, even when not necessary.
Even here we have a Code Analysis rule to identify these cases.
How to improve: the compiler can deduct by itself if it needs to create the state machine, because there is an await keyword inside the method. The async keyword is totally redundant and not necessary.
Further improvement: the compiler can also avoid the creation of the state machine when, for each possible execution path of the method, there is no await instruction, or the await is the last instruction of the execution path - in this case, it can directly return the Task, avoiding the creation of the state machine.
Need to check reference parameters for null
Both with and without nullable reference types enabled, it is recommended to check input reference parameters for nullability.
While it's clear that this is needed when nullable reference types are not enabled, we need to explain why this is needed even in the other case.
The reason is that your code, compiled with nullable reference types, can be called from code not compiled with nullable reference types; or this code could be called via reflection. So, definitely, there are ways to pass null values to input parameters, even when they are marked as non nullable.
Because of these considerations, it is recommended to always check for nullability of parameters.
There has been a proposal, to introduce in C# the double bang operator, with a PR and later discussions. Still I don't like it; the function definitions would become full of these !! (and still the could be sometimes forgotten).
How to improve: in my opinion all of this is redundant. Simply, when nullable reference types are enabled, automatically add code at the beginning of the methods to check for nullability of non-nullable parameters.