Home > Article > Backend Development > .NET Exception Design Principles
Exceptions are inevitable problems when using .NET, but too many developers do not consider this issue from the perspective of API design. In most jobs, they know from start to finish what exceptions need to be caught and which exceptions need to be written to the global log. If you design an API that allows you to use exceptions correctly, you can significantly reduce the time it takes to fix defects.
Whose fault?
The basic theory behind exception design starts with the question, "Whose fault?" For the purposes of this article, the answer to this question will always be one of the following three:
Library
Application
Environment
When we say there is a problem with the "library", we mean that a certain method currently being executed has an internal flaw. In this case, the "application" is the code that calls the library methods (this is a bit confusing because the library and application code may be in the same assembly.) Finally, the "environment" refers to everything outside the application Something that cannot be controlled.
Library defect
The most typical library defect is NullReferenceException. There is no reason for a library to throw a null reference exception that can be detected by the application. If a null is encountered, library code should always throw a more specific exception describing what the null is and how to correct the problem. For parameters, this is obviously an ArgumentNullException. And if the property or field is empty, InvalidOperationException is usually more appropriate.
By definition, any exception that indicates a library flaw is a bug in the library that needs to be fixed. That's not to say that the application code is bug-free, but that the library's bugs need to be fixed first. Only then can the application developer know that he too has made a mistake.
The reason for this is that there may be many people using the same library. If one person mistakenly passes null where they shouldn't, others are bound to make the same mistake. By replacing the NullReferenceException with an exception that clearly shows what went wrong, application developers will know immediately what went wrong.
"The Pit of Success"
If you read the early literature on .NET design patterns, you will often come across the phrase "The Pit of Success." The basic idea is this: make the code easy to use correctly, hard to use incorrectly, and make sure exceptions can tell you what went wrong. By following this API design philosophy, developers are almost guaranteed to write correct code from the start.
This is why an uncommented NullReferenceException is so bad. Other than a stack trace (which may be very deep into the library code), there is no information to help the developer determine what they are doing wrong. ArgumentNullException and InvalidOperationException, on the other hand, provide a way for library authors to explain to application developers how to fix the problem.
Other library defects
The next library defect is the ArithmeticException series, including DivideByZeroException, FiniteNumberException and OverflowException. Again, this always means an internal flaw in the library method, even if that flaw is just a missing parameter validity check.
Another example of a library flaw is IndexOutOfRangeException. Semantically, it is no different from ArgumentOutOfRangeException, see IList.Item, but it only works with array indexers. Since application code usually does not use naked arrays, this means that custom collection classes will have bugs.
ArrayTypeMismatchException has been rare since .NET 2.0 introduced generic lists. The circumstances that trigger this exception are rather weird. According to the documentation:
ArrayTypeMismatchException is thrown when the system cannot convert an array element to the declared array type. For example, an element of type String cannot be stored in an array of Int32 because there is no conversion between the two types. Applications generally do not need to throw such exceptions.
To do this, the Int32 array mentioned earlier must be stored in a variable of type Object[]. If you used a raw array, the library needs to check for this. For this reason and many other considerations, it is better not to use raw arrays but to encapsulate them into a suitable collection class.
Usually, other conversion problems are reflected through InvalidCastException exceptions. Back to our topic, type checking should mean that an InvalidCastException is never thrown, but an ArgumentException or InvalidOperationException is thrown to the caller.
MemberAccessException is a base class that covers various reflection-based errors. In addition to direct use of reflection, COM interop and incorrect use of dynamic keywords can trigger this exception.
Application Defect
Typical application defects are ArgumentException and its subclasses ArgumentNullException and ArgumentOutOfRangeException. Here are other subclasses you may not know about:
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
All of these clearly indicate that the application There is an error, and the problem lies in the line that calls the library method. Both parts of that statement are important. Consider the following code:
foo.Customer = null; foo.Save();
If the above code throws an ArgumentNullException, the application developer will be confused. It should throw an InvalidOperationException indicating that something went wrong before the current line.
Using Exceptions as Documentation
The typical programmer doesn’t read the documentation, at least not in the first place. Instead, he or she reads the public API, writes some code, and runs it. If the code doesn't run properly, search for exception information on Stack Overflow. If the programmer is lucky, it's easy to find the answer there along with a link to the correct documentation. But even then, programmers probably won't actually read it.
So, as library authors, how do we solve this problem? The first step is to copy the partial document directly into the exception.
More Object Status Exceptions
InvalidOperationException has a well-known subclass ObjectDisposedException. Its purpose is obvious, however, few destructible classes forget to throw this exception. If forgotten, a common result is a NullReferenceException. This exception is caused by the Dispose method setting the destructible child object to null.
Closely related to InvalidOperationException is the NotSupportedException exception. The two exceptions are easy to distinguish: InvalidOperationException means "you can't do that now", while NotSupportedException means "you can never do that operation on this class." In theory, NotSupportedException should only occur when using abstract interfaces.
For example, an immutable collection should throw NotSupportedException when encountering the IList.Add method. In contrast, a freezable collection will throw an InvalidOperationException when encountering this method in the frozen state.
An increasingly important subclass of NotSupportedException is PlatformNotSupportedException. This exception indicates that the operation can be performed in some operating environments but not in others. For example, you may need to use this exception when porting code from .NET to UWP or .NET Core, since they do not provide all the features of the .NET Framework.
The Elusive FormatException
Microsoft made some mistakes when designing the first version of .NET. For example, logically FormatException is a parameter exception type, even the documentation says "This exception is thrown when the parameter format is invalid". However, for whatever reason, it doesn't actually inherit ArgumentException. It also has no place to store parameter names.
Our temporary suggestion is not to throw FormatException, but to create a subclass of ArgumentException yourself, which can be named "ArgumentFormatException" or other names with similar effects. This can provide you with necessary information such as parameter names and actual values used, reducing debugging time.
This brings us back to the original theme of "Exceptional Design". Yes, you can just throw a FormatException when your home-grown parser detects a problem, but that won't help application developers who want to use your library.
Another example of this framework design flaw is IndexOutOfRangeException. Semantically, it is no different from ArgumentOutOfRangeException, however, is this special case just for array indexers? No, it would be wrong to think that way. Looking at the instance set of IList.Item, this method will only throw ArgumentOutOfRangeException.
Environmental Defects
Environmental defects stem from the fact that the world is not perfect, such as data downtime, web server unresponsiveness, file loss and other scenarios. When an environmental defect appears in a bug report, there are two aspects to consider:
Did the application handle the defect correctly?
What causes defects in this environment?
Usually, this will involve division of labor. First, application developers should be the first to look for answers to their questions. This doesn't just mean handling errors and recovering, but also generating a useful log.
你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?
专用异常
NotImplementedException
NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:
throw new NotImplementedException("参见工单#42.");
你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。
AggregateException
AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。
由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。
解封装AggregateException
catch (AggregateException ex) { if (ex.InnerExceptions.Count == 1) //解封装 ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //我们真的需要AggregateException }
无法回答的情况
有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。
如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。
例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:
Microsoft.JScript.BreakOutOfFinally
Microsoft.JScript.ContinueOutOfFinally
Microsoft.JScript.JScriptException
Microsoft.JScript.NoContextException
Microsoft.JScript.ReturnOutOfFinally
System.Reflection.InvalidFilterCriteriaException
System.Reflection.TargetException
System.Reflection.TargetInvocationException
System.Reflection.TargetParameterCountException
System.Threading.WaitHandleCannotBeOpenedException
显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。
同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。
无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。
实践:重新设计SqlException
请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。
如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。
SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。
SqlClient.InternalException will contain an error code indicating a serious failure of the server (such as database corruption or inability to access the hard disk).
SqlClient.SyntaxException is equivalent to our ArgumentException. It means you are passing bad SQL to the server (either directly or because of an ORM bug).
SqlClient.MissingObjectException occurs when the syntax is correct but the database object (table, view, stored procedure, etc.) does not exist.
SqlClient.DeadlockException occurs when two or more processes conflict when trying to modify the same information.
Each of these exceptions implies a course of action.
SqlClient.NetworkException: Retry the operation. If it occurs frequently, please contact operation and maintenance personnel.
SqlClient.InternalException: Contact the DBA immediately.
SqlClient.SyntaxException: Notify the application or database developer.
SqlClient.MissingObjectException: Please ask the operation and maintenance personnel to check whether anything was lost in the last database deployment.
SqlClient.DeadlockException: Retry the operation. If it happens frequently, look for design errors.
If we were to do this in a real job, we would have to map all 11,000+ SQL Server error codes to one of those categories, which is particularly daunting works, which explains why SqlException is what it is now.
Summary
When designing an API, to make it easier to correct problems, organize exceptions according to the type of action that needs to be performed. This makes it easier to write self-corrected code, keep more accurate logs, and communicate issues to the right person or team faster.
About the author
Jonathan Allen began participating in MIS projects for medical offices in the late 1990s, gradually elevating them from Access and Excel to an enterprise level solution. He spent five years coding automated trading systems for the financial industry before deciding to move into high-end user interface development. In his spare time, he likes to study and write about Western fighting techniques from the 15th to 17th centuries.
The above is the content of .NET exception design principles. For more related content, please pay attention to the PHP Chinese website (www.php.cn)!