We are creating a new .Net Core Library, an application service that resides within a Clean Architecture.
The new service will read new customer details via the RepositoryService layer and Post them via a RestApiService Layer to multiple systems including : CreditCheck system, Billing system etc..
Within the new Application Service we want a consistent way of handling responses from RestAPi service. Response data includes:
- Return values: entities returned by restAPI service
- Exceptions like an error 500, time outs.. that have bubbled up from the RestAPi.
- Data Errors messages such as Customer already exists, bank details invalid
- Warning messages ... "Processing continues as normal, but order is flagged"
Microsoft and SOLID virtually state that the use of exceptions handling is the way to go , whether it be exceptions, errors or warnings.
But in this scenario not clear how this will work ..
a. We loose the option of handling and forwarding on the return values.
We really don't fancy storing all this in the exception message
- Whilst not a show stopper, we fear the code will be more difficult to read than it needs to be.
- Exception handling is expensive,but not worried too much on this score with number of transactions.
We are drawn to some how using FluentValidation or a hybrid version, it will need to work with Logging and RepositoryService as we will need log and decode stuff .
We really don't fancy repeating the RestAPi Service layer approach i.e. handling HTTP exceptions separately, then processing return values which are basically extended entities with Errors Status , Error codes and messages.
So our question is how can we best handle Errors warnings along side data in the application service layer and still have SOLID testable and maintainable code ?
Microsoft and SOLID are right.
The correct way to go are Exceptions as per standard practices (and c#), independently of another considerations, like performance for instance.
Generally speaking there are two different types of "Errors", technical, and business logic related.
Failing to Connect to a DB, receiving a 500 from a REST Service, etc... are technical, and as they could be transient you can try to recover from this situation, sometimes without success, what finally causes the Business Orchestration/Process failure.
Business Logic errors, like 'Customer already exists', 'Bank details invalid','Input data is not in the valid format' are non-transient, and determined solely by Business Rules (some implicit, other explicit) and will stop stop your process as well without possibility to recover, simply because something is not in the proper/expected state.
There is a reason we use Exceptions (as technical artifacts) and is to handle these hard-stops 'properly'.
Every time you throw an Exception the application traverse back the stack up to the first available Exception Handler able to handle such Exception, returning the control to you and to a known location where something will happen (telemetry, rethrow, show a dialog to the user...)
Any mechanism trying to substitute this propagation (Error) must rely, for instance, on hijacking the return value of the methods to provide back a status, or forcibly include an out parameter to all your methods signatures, which will have awful collateral effects in your implementation.
Yes, sometimes your current design look 'too flat' that you are tempted to hijack the return value here and there, creating a highly coupled component, but you never can tell how the system complexity will grow, meaning that at some point you will extend your system with additional layers, and most likely the 'substituting mechanism' approach will not fit , and you are forced to support them, the regular way and the imaginative one.
So, trying to implement a custom solution will create a tightly-coupled technical requirement that must be supported all across the board, what in architecture terms is simply 'not good' (not a good practice)
IF any Service you are consuming is not able to produce a meaningful 'error' information, well structured, then, the Service is not well designed, and the problem flips to the Client side (Talking in SOA terms).
The only 'most correct' solution left to not introduce chaos in your Client is by creating a Proxy to the Service RESPECTING the rules required by your implementation approach.
My recommendation about Error Handling is very simple, stick to the rules that are well know and have been in place for decades and do not try workaround this by yourself, or you are going to face too many issues, create a Proxy for each service and integrate them properly in your code base (unit testing and such)
In regards to warnings, there is no recommendation from anybody to handle this by using Exceptions, those 'warnings' in your question is a normal output when interacting to the Service/a logical state supported by the Business Logic, thus, the place to handle this state is in the Data Contract coming back from the Service, again, if the Service is so horribly designed that is replying you with just a text string containing the word 'warning', and you are forced to parse this string to notice the situation, then your Proxy to the Service should check for it and provide a meaningful and well structured output to the Client.