ErrorProne.NET. Part 3
In C# there are quite a lot of features displayed as a rather difficult IL code that lead to behavior not always obvious for the users / code readers. A good example of this is new() exception in generalizations, the use of which leads to using reflection and creating new objects with Activator.CreateInstance, changing the exception's "profile" and negatively affecting performance.
Apart from that, there are a couple more features with very similar implementation and curious effects in terms of exception processing.
Preconditions in iterator block
The C# compiler turns the iterator block into an ultimate automatic tool for producing behavior widely recognized in some circles as continuation passing style. There is nothing complicated in the constructions themselves, but problems may easily occur when they are innocently, but incorrectly used.
Let us consider such an example, a rather primitive one.
It's not perfect, but certainly could happen. At the beginning of the method is the argument validation, and then we open the file and read its contents line by line. The main issue is the time it takes to generate exceptions. How obvious is it, from the first glance, when it will occur?
As the iterator block is not a common method, the exception will occur only when the iterator "materializes," so it will come out in line 3. As the iterator block is executed lazily, the precondition check will be done only at the first call for MoveNext method on the obtained iterator. This isn't bad when there is only one code line between obtaining the iterator and using it – then it will not be very difficult to understand the initial reason. But in practice, IEnumerable<T> can be saved or transferred to other subsystem, which makes it rather difficult to understand what went and when it went.
This problem is solved in a rather typical way: the method needs to be broken down in two, with the first one left as the precondition check and the main work "shifted" to the separate method.
It is not difficult to guess that ErrorProne.NET has a special rule for catching such cases and a fixer that can divide the method into two.
Preconditions in asynchronous methods
Preconditions in iterator blocks can seem rather exotic, but the same problem is inherent to the another language structure — asynchronous methods, i.e. methods identified by the keyword async.
Any method contains a formal or informal contract. If the requesting party fulfills a certain condition called a precondition, the method will do its best to fulfill its obligations (like trying to guarantee its postconditions). Considering the fulfillment time, violation of preconditions is usually modeled using the exception generation ArgumentException (or derivative classes) while other exception types model the implementation problems and can be construed as a postcondition violation.
Exclusion type and fulfillment time allow us to get a better understanding of the obligations of different components and figure out who was wrong and what to do. However, when it comes to asynchronous methods, everything becomes a little more difficult.
With the arrival of TAP (Task-based async pattern), the method obtained two ways of informing about a problem that has occurred. The method can throw the exclusion during its call or return the task "broken". Precondition violation exclusions are synchronous and inform the method client about the failure to fulfill its part of the obligations. Exclusion synchronism will let him know that he was wrong and that the operation was not even begun. A broken task indicates a problem in fulfilling the tasks in terms of implementation.
Now let us return to the asynchronous method.
At what moment will the ANE exclusion be thrown?
Asynchronous methods are realized in such a way that the part of the method before the first word await is done synchronously, but if an exclusion occurs at this moment, it will not be thrown at the client directly. Instead, the method will be completed "successfully" and the exclusion will be forwarded at the moment this result is received from the task.
The problem of such behavior is the same as the iterator block problem. We can obtain a task, save it in the field, transfer it to another method and observe the ArgumentNullException result in the opposite part of the program. Yet, in fact, the task was not even run as the preconditions were not fulfilled.
In this case, the same method allocation trick is used: break the method into two, delete the async keyword from the first one but leave the precondition check and allocate all the main work to the auxiliary method.
Always remember the rules
Each time I changed teams I started cleaning up the coding guidelines, because in most cases they mentioned nothing about good exception processing practice. Now, when you've got clever analyzers, most of the rules can be easily automated and the compilation can simply be broken when they are not fulfilled.
(Yes, I know about the FxCop custom rules, but I never saw custom rules for correct exclusion processing. Additionally, FxCop is not maintained by anyone in particular, so no one can guarantee it will be compatible with the latest compiler version, because it uses the IL analysis level, which is very difficult to analyze asynchronous methods on).
Preconditions and contracts
All of my suggestions about preconditions/postconditions are confirmed by the fact that tools for design by contract, namely, Code Contracts, process the iterator block and asynchronous method preconditions in a peculiar way. They do the same logical conversions explained here: namely, they cause the precondition violations to "trigger" synchronously (eagerly) and be thrown to the client at the moment the method is called, and not at the moment the result is processed!
Sergey Teplyakov
Expert in .Net, С++ and Application Architecture