How to work with exceptions in DDD

 3r33565. 3r3-31. How to work with exceptions in DDD 3r33552.  3r33565. 3r33552.  3r33565. As part of a recently held conference 3–3–37. DotNext 2018
BoF took place on Domain Driven Design. It raised the issue of working with exceptions, which caused a heated debate, but did not get a detailed discussion, since it was not the main topic. 3r33552.  3r33565. 3r33552.  3r33565. Also, studying a variety of resources, ranging from questions on stackoverflow and ending with paid courses on architecture, one can observe that the IT community has developed an ambiguous attitude towards exceptions and how to use them. 3r33552.  3r33565. 3r33552.  3r33565. Most often it is mentioned that with the use of exceptions it is easy to build a thread of execution, having the semantics of the goto operator which is bad for code readability. 3r33552.  3r33565. 3r33552.  3r33565. There are different opinions about whether it is worth create your own exception types or use the standard supplied in .NET. 3r33552.  3r33565. 3r33552.  3r33565. Someone does validation on exceptions, and someone else does uses the Result monad . It is fair that Result allows us to understand by the method signature whether it is possible not only successful execution. But it is no less true that in imperative languages ​​(which include C #), the widespread use of Result leads to poorly readable code, filled with language constructs so that it is difficult to discern the original script. 3r33552.  3r33565. 3r33552.  3r33565. In this article I will talk about the practices adopted in our team (if briefly - we use all the approaches and none of them is a dogma). 3r33552.  3r33565. 3r33552.  3r33565. It's about an enterprise application built on the ASP.NET MVC + WebAPI base. The application is built on onion architecture 3-333549. Communicates with a database and message broker. Structured logging to ELK stack is used. and configured monitoring with Grafana. 3r33552.  3r33565.
3r3-3549. 3r33552.  3r33565. We will look at work with exceptions from three angles: 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. General rules for dealing with exceptions
 3r33565. 3r33434. Exceptions, Errors, and Onion Architecture
 3r33565. 3r33434. Special cases for web applications
 3r33565.
3r33552.  3r33565. 3r33552.  3r33565.

General rules for dealing with exceptions

3r33552.  3r33565.
 3r33565. 3r33434. Exceptions and errors are not the same thing. For exceptions use exceptions, for errors - Result. 3rr3465.  3r33565. 3r33434. Exceptions are only for exceptional situations, which by definition cannot be many. This means there are fewer exceptions - the better. 3rr3465.  3r33565. 3r33434. Exception handling should be as granular as possible. As Richter wrote in his monumental work. 3rr3465.  3r33565. 3r33434. If the error should be delivered to the user in its original form - use Result. 3rr3465.  3r33565. 3r33434. The exception should not leave the system boundaries in their original form. It is not user friendly and gives the attacker a way to further explore possible system weaknesses. 3rr3465.  3r33565. 3r33434. If the thrown exception is processed by our application, we use not exception, but Result. An implementation on exceptions will be a hidden goto statement and the worse the processing code is from the exception code of an exception, the worse it will be. Result, on the other hand, explicitly declares the possibility of an error and allows only “linear” processing. 3rr3465.  3r33565.
3r33552.  3r33565. 3r33552.  3r33565.

Exceptions, Errors, and Onion Architecture

3r33552.  3r33565. In the following sections, we consider the responsibilities and rules for throwing /handling exceptions /errors for the following layers: 3r3552.  3r33565.
 3r33565. 3r33434. Application Hosts
 3r33565. 3r33434. Infrastructure
 3r33565. 3r33434. Application Services
 3r33565. 3r33434. Domain core
 3r33565.
3r33552.  3r33565. 3r33333. Application Host
3r33552.  3r33565. 3r33411. What 3r??? is responsible for. 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. 3r3-33132. Composition root
customizing the work of the entire application. 3rr3465.  3r33565. 3r33434. The boundary of interaction with the outside world - users, other services, scheduled launch. 3rr3465.  3r33565.
3r33552.  3r33565. Since these are quite complex responsibilities, it is worthwhile to limit them The remaining responsibility is given to the inner layers. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle errors from Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. Transmits to the outside world, transforming it into an appropriate format (for example, in http response). 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to generate Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. No This layer does not contain logic, so there is no place to generate errors. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. Hides the details and converts to a format suitable for sending to the outside world
 3r33565. 3r33434. Logs. 3rr3465.  3r33565.
3r33552.  3r33565. 3r33411. Like throwing exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. No, this layer is the outermost one and contains no logic - there is no one to give an exception to it. 3r33552.  3r33565. 3r33552.  3r33565. 3r33333. Infrastructure
3r33552.  3r33565. 3r33411. What 3r??? is responsible for. 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. Adapters to ports , or simply implementations of Domain-interfaces, giving access to the infrastructure - third-party services, databases, active directory, etc. This layer should be as “stupid” as possible and contain as little logic as possible. 3rr3465.  3r33565. 3r33434. If necessary, can act as Anti-corruption layer . 3rr3465.  3r33565.
3r33552.  3r33565. 3r33411. How to handle errors from Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. I am not aware of providers for databases and other services running on the Result monad. However, some services operate on return codes. In this case, convert them to the Result format required by the port. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to generate Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. In the general case, this layer does not contain logic, which means it does not generate errors. 3r33552.  3r33565. But in the case of use as an anti corruption layer, a variety of options are possible. For example, parsing exceptions from the legacy service and converting to Result those exceptions that are simple validation messages. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. In general, throws further, if necessary, pledge details. 3r33552.  3r33565. If the port being implemented allows Result to be returned in the contract, then the infrastructure converts the types of exceptions that can be handled to Result. 3r33552.  3r33565. 3r33552.  3r33565. For example, a message broker used in a project throws exceptions when trying to send a message when the broker is unavailable. The Application Services layer is ready for this situation and is able to handle it with the policy of Retry, Circuit Breaker or manual data rollback. 3r33552.  3r33565. 3r33552.  3r33565. In this case, the Application Services layer declares a contract that returns a Result in case of an error. And the Infrastructure layer implements this port, converting the exception from the broker to Result. Naturally, converts only specific types of exceptions, and not everything. 3r33552.  3r33565. 3r33552.  3r33565. Using this approach, we get two advantages: 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. Explicitly declare the possibility of errors in the contract. 3rr3465.  3r33565. 3r33434. We get rid of the situation when the Application Service knows how to handle the error, but does not know the type of the exception, because it is abstracted from a particular message broker. At the same time, building a catch block on the base System.Exception means to capture all types of exceptions, and not just those that the Application Service can handle. 3rr3465.  3r33565.
3r33552.  3r33565. 3r33411. Like throwing exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. Depends on the specifics of the system. 3r33552.  3r33565. 3r33552.  3r33565. For example, the LINQ operators Single and First, when querying for non-existent data, throw an InvalidOperationException exception. But this type of exception is used everywhere in .NET, which makes it impossible to process it granularly. 3r33552.  3r33565. 3r33552.  3r33565. We in the team have taken the practice of creating custom ItemNotFoundException and throwing it from the infrastructure layer if the requested data is not found and so it should not be according to business rules. 3r33552.  3r33565. 3r33552.  3r33565. If the requested data is not found and it is acceptable - it is worth explicitly declaring it in the port contract. For example, using r3r3293. monads Maybe
. 3r33552.  3r33565. 3r33552.  3r33565. 3r33333. Application Services
3r33552.  3r33565. 3r33411. What 3r??? is responsible for. 3r33552.  3r33565. 3r33552.  3r33565.
 3r33565. 3r33434. Validation of input data. 3rr3465.  3r33565. 3r33434. Orchestration and coordination of services - starting and completing transactions, implementing distributed scripts, etc. 3rr3465.  3r33565. 3r33434. Loading domain-objects and external data through the ports to the Infrastructure, the subsequent call commands in the Domain Core. 3rr3465.  3r33565.
3r33552.  3r33565. 3r33411. How to handle errors from Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. Errors from the domain core transmits to the outside world without changes. Errors from Infrastructure can process through policies Retry, Circuit Breaker or broadcast outside. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to generate Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. It can implement validation in the form of Result. 3r33552.  3r33565. 3r33552.  3r33565. May generate notifications of partial success of the operation. For example, messages to the user of the form “Your order was successfully placed, but an error occurred while verifying the delivery address. A specialist will contact you shortly to clarify the details of the delivery. ”3r3-33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. Assuming that the infrastructure exceptions that the application is able to process have already been transformed by the Infrastructure layer into Result - it does not handle at all. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. Like throwing exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. In general, no way. But there are borderline options described in the final section of the article. 3r33552.  3r33565. 3r33552.  3r33565. 3r33333. Domain core
3r33552.  3r33565. 3r33411. What 3r??? is responsible for. 3r33552.  3r33565. 3r33552.  3r33565. The implementation of business logic, the “core” of the system and the main purpose of its existence. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle errors from Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. Since the layer is internal and errors are possible only from objects in the same domain, processing is reduced either to business rules or to broadcasting the error upward in its original form. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to generate Result [/b] 3r33552.  3r33565. 3r33552.  3r33565. If you violate business rules that are encapsulated in the Domain Core and are not covered by the validation of input data at the Application Services level. In general, in this layer Result is used most often. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. How to handle exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. No Exceptions to the infrastructure have already been processed by the Infrastructure layer, the data have already come structured, complete and verified through the Application Services layer. Accordingly, all the exceptions that can take off are truly exceptions. 3r33552.  3r33565. 3r33552.  3r33565. 3r33411. Like throwing exceptions [/b] 3r33552.  3r33565. 3r33552.  3r33565. Usually the general rule works here: the fewer exceptions, the better. 3r33552.  3r33565. 3r33552.  3r33565. But have you ever had a situation where you write code and understand that under certain conditions it can mess things up? For example, twice to write off the money or so spoil the data that then we will not collect the bones. 3r33552.  3r33565. 3r33552.  3r33565. As a rule, we are talking about the execution of commands that are unacceptable for the current state of the object. 3r33552.  3r33565. 3r33552.  3r33565. Of course, the corresponding button on the UI should not be visible in this state. We should not get a command from the bus in this state. All this is true provided that the outer layers and systems performed their function 3r33518. normally [/i] . But in Domain Core we do not need to know about the existence of external layers and believe in the correctness of their work, we must protect the invariants of the system. 3r33552.  3r33565. 3r33552.  3r33565. Some of the checks can be placed in Application Services at the level of validation. But it could turn into defensive programming which in extreme cases leads to the following: 3r35252.  3r33565.
 3r33565. 3r33434. Encapsulation is weakened, since certain invariants must be tested on the outer layer. 3rr3465.  3r33565. 3r33434. Knowledge of the subject area “flow” into the outer layer, checks can be duplicated by both layers. 3rr3465.  3r33565. 3r33434. Check 3r33535. admissibility [/i] executing a command from the outer layer can be more complicated and less reliable than checking a domain object 3r-3518. impossibility 3r3195. execute the command in the current state. 3rr3465.  3r33565.
3r33552.  
Also, if we place such checks in the validation layer, then we must tell the user the reason for the error. Considering that we are talking about an operation that cannot be performed in the current conditions at all, we risk being in one of two situations of a situation: 3r3552.  3r33565.
 3r33565. 3r33434. We gave a message to an ordinary user, which he didn’t understand at all and would still go to support, as with the message "An unexpected error has occurred." 3rr3465.  3r33565. 3r33434. We quite clearly told the villain why he could not perform the operation he wanted to perform and he could look for other workarounds. 3rr3465.  3r33565.
3r33552.  3r33565. But back to the main topic of the article. By all indications, the situation under discussion is exceptional. It should never happen, but if it does, it will be bad. 3r33552.  3r33565. 3r33552.  3r33565. In this situation, it is most logical to throw an exception, log the necessary details, return the general error “Operation Impossible” to the user, set up monitoring for this type of errors and expect that we will never see them. 3r33552.  3r33565. 3r33552.  3r33565. What type or types of exceptions to use in this case? Logically, it should be a separate type of exception, so that we can distinguish it from others and not accidentally hooked it with exception handling from the outer layer. We, too, do not need a hierarchy or many exceptions, the essence is the same — something unacceptable has happened. We in our projects create for this the type CorruptedInvariantException, and use it in appropriate situations. 3r33552.  3r33565. 3r33552.  3r33565.

Special cases for Web applications

3r33552.  3r33565. A significant difference of web applications from others (desktop, demons and windows services, etc.) is interaction with the outside world in the form of short-term operations (processing HTTP requests), after which the application “forgets” about what happened. 3r33552.  3r33565. 3r33552.  3r33565. Also, after the completion of the processing of a request, a response is always generated. If the operation performed by our code does not return data, the platform will still return a response containing the status code. If the operation was interrupted by an exception, the platform will still return a response containing the corresponding status code. 3r33552.  3r33565. 3r33552.  3r33565. To implement this behavior, the processing of requests in Web platforms is built in the form of pipelines (pipe). At first, the request is processed and then the response is prepared. 3r33552.  3r33565. 3r33552.  3r33565. We can use middleware, action filter, http handler or ISAPI filter (depending on the platform) and integrate into this pipeline at any stage. And at any stage of processing the request, we can interrupt processing and the pipeline will proceed to the formation of a response. 3r33552.  3r33565. 3r33552.  3r33565. The business part of the application, we, as a rule, are no longer implemented in the pipeline architecture, but we write code that performs operations sequentially. And with this approach, it is somewhat more difficult to implement the script when we interrupt the execution of the request and immediately proceed to form the answer. 3r33552.  3r33565. 3r33552.  3r33565. What does all this have to do with exception handling, you ask? 3r33552.  3r33565. 3r33552.  3r33565. The rules for working with exceptions described in the previous parts of the article do not fit well in this scenario. 3r33552.  3r33565. It is bad to use exceptions because it is goto semantics. 3r33552.  3r33565. Ubiquitous use of Result leads to the fact that we drag it (Result) across all layers of the application, and when forming the answer, we need to sort Result somehow in order to understand which return status code. It is also desirable to summarize and parse this parsing code in Middleware or ActionFilter, which becomes a separate adventure. That is, Result is not much better than exceptions. 3r33552.  3r33565. 3r33552.  3r33565. 3r33518. What to do in this situation? 3r? 3519. 3r33552.  3r33565. 3r33552.  3r33565. Do not build an absolute. We set the rules for our own benefit, not harm. 3r33552.  3r33565. 3r33552.  3r33565. If it is necessary to interrupt the operation, because its continuation is impossible, then an exception throw will not have goto semantics. We direct execution to the output, and not to another block of business code. 3r33552.  3r33565. 3r33552.  3r33565. If the reason for the interruption is important to determine the desired status code, you can use custom exception types. 3r33552.  3r33565. 3r33552.  3r33565. Earlier, we mentioned two custom types that we use: ItemNotFoundException (transform into 404) and CorruptedInvariant (transform into 500). 3r33552.  3r33565. 3r33552.  3r33565. If you check the rights of users, because they do not fall on the role model or claim, then it is permissible to create a custom ForbiddenException (Status code 403). 3r33552.  3r33565. 3r33552.  3r33565. And finally, validation. We still can not do anything until the user modifies his query, this semantics 3r-3544. described by the code 422
. So we interrupt the operation and send the request straight to the output. It is also valid to do using exception. For example, in the library 3-333546. FluentValidation
There are already 3r33548. built-in exception type
, which sends to the client all the details necessary to clearly display to the user what is wrong with the request. 3r33552.  3r33565. 3r33552.  3r33565. That's all. And how do you work with exceptions? 3r33561. 3r33565. 3r33565. 3r33565. 3r33558. ! function (e) {function t (t, n) {if (! (n in e)) {for (var r, a = e.document, i = a.scripts, o = i.length; o-- ;) if (-1! == i[o].src.indexOf (t)) {r = i[o]; break} if (! r) {r = a.createElement ("script"), r.type = "text /jаvascript", r.async =! ? r.defer =! ? r.src = t, r.charset = "UTF-8"; var d = function () {var e = a.getElementsByTagName ("script")[0]; e.parentNode.insertBefore (r, e)}; "[object Opera]" == e.opera? a.addEventListener? a.addEventListener ("DOMContentLoaded", d,! 1): e.attachEvent ("onload", d ): d ()}}} t ("//mediator.mail.ru/script/2820404/"""_mediator") () (); 3r3-3559. 3r33565. 3r33561. 3r33565. 3r33565. 3r33565. 3r33565.
.NET / C#
+ 0 -

Add comment