This chapter contains recipes covering the exception-handling mechanism, including the try
, catch
, and finally
blocks. Along with these recipes are others covering the mechanisms used to throw exceptions manually from within your code. The final recipes deal with the Exception
classes and their uses, as well as subclassing them to create new types of exceptions.
Often, the design and implementation of exception handling is performed later in the development cycle. But with the power and complexities of C# exception handling, you need to plan and even implement your exception-handling scheme much earlier. Doing so will increase the reliability and robustness of your code while minimizing the impact of adding exception handling after most or all of the application is coded.
Exception handling in C# is very flexible. It allows you to choose a fine- or coarse-grained approach to error handling, or any level between. This means that you can add exception handling around any individual line of code (the fine-grained approach) or around a method that calls many other methods (the coarse-grained approach), or you can use a mix of the two, with mainly a coarse-grained approach and a more fine-grained approach in specific critical areas of the code. When using a fine-grained approach, you can intercept specific exceptions that might be thrown from just a few lines of code. The following method sets an object’s property to a numeric value using fine-grained exception handling:
protected void SetValue(object value) { try { myObj.Property1 = value; } catch (NullReferenceException) { // Handle potential exceptions arising from this call here. } }
Consequently, this approach can add a lot of extra baggage to your code if used throughout your application. This fine-grained approach to exception handling should be used when you have a single line or just a few lines of code, and you need to handle that exception in a specific manner. If you do not have specific handling for errors at that level, you should let the exception bubble up the stack. For example, using the previous SetValue
method, you may have to inform the user that an exception occurred and provide a chance to try the action again. If a method exists on myObj
that needs to be called whenever an exception is thrown by one of its methods, you should make sure that this method is called at the appropriate time.
Coarse-grained exception handling is quite the opposite; it uses fewer try
-catch
or try
-catch
-finally
blocks. One example of a coarse-grained approach would be to place a try
-catch
block around all of the code in every public
method in an application or component. Doing this allows exceptions to be handled at the highest level in your code. If an exception is thrown at any location in your code, it will be bubbled up the call stack until a catch
block is found that can handle it. If try
-catch
blocks are placed on all public
methods, then all exceptions will be bubbled up to these methods and handled. This allows you to write much less exception-handling code, but it diminishes your ability to handle specific exceptions that may occur in particular areas of your code. You must determine how best to add exception-handling code to your application. This means applying the right balance of fine- and coarse-grained exception handling in your application.
C# allows you to write catch
blocks without any parameters. An example of this is shown here:
public void CallCOMMethod() { try { // Call a method on a COM object. myCOMObj.Method1(); } catch { //Handle potential exceptions arising from this call here. } }
The catch
with no parameters is a holdover from C++, where exception objects did not have to be derived from the Exception
class. Writing a catch
clause in this manner in C++ allows any type of object thrown as an exception to be caught. However, in C#, only objects derived from the Exception
base class may be thrown as an exception. Using the catch
block with no parameters allows all exceptions to be caught, but you lose the ability to view the exception and its information. A catch
block written in this manner:
catch { // NOT able to write the following line of code //Console.WriteLine(e.ToString); }
is equivalent to this:
catch (Exception e) { // Able to write the following line of code Console.WriteLine(e.ToString); }
except that in the second case, the Exception
object can be accessed now that the exception parameter is provided.
Avoid writing a catch
block without any parameters. Doing so will prevent you from accessing the actual Exception
object that was thrown.
When catching exceptions in a catch
block, you should determine up front when exceptions need to be rethrown, when exceptions need to be wrapped in an outer exception and thrown, and when exceptions should be handled immediately and not rethrown.
Wrapping an exception in an outer exception is a good practice when the original exception would not make sense to the caller. When wrapping an exception in an outer exception, you need to determine what exception is most appropriate to wrap the caught exception. As a rule of thumb, the wrapping exception should always aid in tracking down the original problem by not obscuring the original exception with an unrelated or vague wrapping exception. One of the rare cases that can justify obscuring exceptions is if the exception is going to cross a trust boundary, and you have to obscure it for security reasons.
Another useful practice when catching exceptions is to provide catch
blocks to handle specific exceptions in your code. And remember that base class exceptions—when used in a catch
block—catch not only that type, but also all of its subclasses.
The following code uses specific catch
blocks to handle different exceptions in the appropriate manner:
public void CallCOMMethod() { try { // Call a method on a COM object. myCOMObj.Method1(); } catch (System.Runtime.InteropServices.ExternalException) { // Handle potential COM exceptions arising from this call here. } catch (InvalidOperationException) { // Handle any potential method calls to the COM object that are // not valid in its current state. } }
In this code, ExternalException
and its derivatives are handled differently than InvalidOperationException
and its derivatives. If any other types of exceptions are thrown from the myCOMObj.Method1
, they are not handled here, but are bubbled up until a valid catch
block is found. If no valid catch
block is found, the exception is considered unhandled and the application terminates.
At times, cleanup code must be executed regardless of whether an exception is thrown. Any object must be placed in a stable known state when an exception is thrown. In these situations, when code must be executed, use a finally
block. The following code has been modified (see boldface lines) to use a finally
block:
public void CallCOMMethod() { try { // Call a method on a COM object. myCOMObj.Method1(); } catch (System.Runtime.InteropServices.ExternalException) { // Handle potential COM exceptions arising from this call here. } finally { // Clean up and free any resources here. // For example, there could be a method on myCOMObj to allow us to clean // up after using the Method1 method. } }
The finally
block will always execute, no matter what happens in the try
and catch
blocks. The finally
block executes even if a return
, break
, or continue
statement is executed in the try
or catch
blocks or if a goto
is used to jump out of the exception handler. This allows for a reliable method of cleaning up after the try
(and possibly catch
) block code executes.
The finally
block is also very useful for final resource cleanup when no catch
blocks are specified. This pattern would be used if the code being written can’t handle exceptions from calls it is making but wants to make sure that resources it uses are cleaned up properly before moving up the stack. The following example makes sure that SqlConnection
and SqlCommand
are cleaned up properly in the finally
block through the use of the using
keyword, which wraps a try
-finally
block around the scope of the using
statement:
public static int GetAuthorCount(string connectionString) { SqlConnection sqlConn = null; SqlCommand sqlComm = null; using(sqlConn = new SqlConnection(connectionString)) { using (sqlComm = new SqlCommand()) { sqlComm.Connection = sqlConn; sqlComm.Parameters.Add("@pubName", SqlDbType.NChar).Value = "O''Reilly"; sqlComm.CommandText = "SELECT COUNT(*) FROM Authors " + "WHERE Publisher=@pubName"; sqlConn.Open(); object authorCount = sqlComm.ExecuteScalar(); return (int)authorCount; } } }
When determining how to structure exception handling in your application or component, consider doing the following:
Use a single try
-catch
or try
-catch
-finally
exception handler at locations higher up in your code. These exception handlers can be considered coarse-grained.
Code farther down the call stack should contain try
-finally
exception handlers. These exception handlers can be considered fine-grained.
The fine-grained try
-finally
exception handlers allow for better control over cleanup after an exception occurs. The exception is then bubbled up to the coarser-grained try
-catch
or try
-catch
-finally
exception handler. This technique allows for a more centralized scheme of exception handling and minimizes the code that you have to write to handle exceptions.
To improve performance, you should handle the case when an exception could be thrown (rather than catch the exception after it is thrown) if you know the code will be run in a single-threaded environment. If the code will run on multiple threads, there is still the potential that the initial check could succeed, but the object value could change (perhaps to null
) in another thread before the actions following the check can be taken.
For example, in a single-threaded environment, if a method has a good chance of returning a null
value, you should test the returned value for null
before that value is used, as opposed to using a try
-catch
block and allowing the NullReferenceException
to be thrown. If you think a null
value is possible, check for it. If it shouldn’t happen, then it is an exceptional condition when it does, and exception handling should be used. To illustrate this, the following method uses exception-handling code to process the NullReferenceException
:
public void SomeMethod() { try { Stream s = GetAnyAvailableStream(); Console.WriteLine("This stream has a length of " + s.Length); } catch (NullReferenceException) { // Handle a null stream here. } }
Here is the method implemented to use an if
-else
conditional instead:
public void SomeMethod() { Stream s = GetAnyAvailableStream(); if (s != null) { Console.WriteLine("This stream has a length of " + s.Length); } else { // Handle a null stream here. } }
Additionally, you should make sure that this stream is closed by using the finally
block as follows:
public void SomeMethod() { Stream s = null; using(s = GetAnyAvailableStream()) { if (s != null) { Console.WriteLine("This stream has a length of " + s.Length); } else { // Handle a null stream here. } } }
The finally
block contains the method call that will close the stream, ensuring that there is no data loss.
Consider throwing exceptions instead of returning error codes. With well-placed exception-handling code, you should not have to rely on methods that return error codes, such as a Boolean true
-false
, to correctly handle errors, making for much cleaner code. Another benefit is that you do not have to look up any values for the error codes to understand the code.
The biggest advantage to exceptions is that when an exceptional situation arises, you cannot just ignore it as you can with error codes. This helps you find and fix bugs.
Throw the most specific possible exception, not general ones. For example, throw an ArgumentNullException
instead of an ArgumentException
, which is the base class of ArgumentNullException
. Throwing an ArgumentException
just tells you that there was a problem with a parameter value to a method. Throwing an ArgumentNullException
tells you more specifically what the problem with the parameter really is. Another potential problem is that a more general exception may not be caught if the catcher of the exception is looking for a more specific type derived from the thrown exception.
The FCL provides several exception types that you will find very useful to throw in your own code. Many of these exceptions are listed here with a definition of where and when they should be thrown:
Throw an InvalidOperationException
in a property, indexer, or method when it is called with the object in an inappropriate state (e.g., when an indexer is called on an object that has not yet been initialized or methods are called out of sequence).
Throw ArgumentException
if invalid parameters are passed into a method, property, or indexer. The ArgumentNullException
, ArgumentOutOfRangeException
, and InvalidEnumArgumentException
are three subclasses of the ArgumentException
class. It is more appropriate to throw one of these subclassed exceptions because they are more indicative of the root cause of the problem. The ArgumentNullException
indicates that a parameter was passed in as null
and that this parameter cannot be null
under any circumstance. The ArgumentOutOfRangeException
indicates that an argument was passed in that was outside of a valid acceptable range. This exception is used mainly with numeric values. The InvalidEnumArgumentException
indicates that an enumeration value was passed in that does not exist in that enumeration type.
Throw a FormatException
when an invalid formatting parameter is passed in to a method. You’d use this technique mainly when overriding/overloading methods such as ToString
that can accept formatting strings, as well as in the parse methods on the various numeric types.
Throw ObjectDisposedException
when a property, indexer, or method is called on an object that has already been disposed.
Many exceptions that derive from the SystemException
class, such as NullReferenceException
, ExecutionEngineException
, StackOverflowException
, OutOfMemoryException
, and IndexOutOfRangeException
, are thrown only by the CLR and should not be explicitly thrown with the throw
keyword in your code.
The .NET Framework Class Library (FCL) also contains many classes to obtain diagnostic information about your application, as well as the environment in which it is running. In fact, there are so many classes that a namespace, System.Diagnostics
, was created to contain all of them. This chapter includes recipes for instrumenting your application with debug/trace information, obtaining process information, using the built-in event log, and taking advantage of mechanisms like performance counters or Event Tracing for Windows (ETW) and EventSource
. It should be noted that ETW and EventSource
are becoming the preferred performance telemetry mechanism for the .NET Framework.
Debugging (via the Debug
class) is turned on by default in debug builds only, and tracing (via the Trace
class) is turned on by default in both debug and release builds. These defaults allow you to ship your application instrumented with tracing code using the Trace
class. You ship your code with tracing compiled in but turned off in the configuration so that the tracing code is not called (for performance reasons) unless it is a server-side application (where the value of the instrumentation may outweigh the performance hit, and in the cloud, nobody can hear you scream without logs!). If a problem occurs on a production machine and you cannot re-create it on your development computer, you can enable tracing and allow the tracing information to be dumped to a file. You can then inspect this file to help you pinpoint the real problem.
Since both the Debug
and Trace
classes contain the same members with the same names, you can interchange them in your code by renaming Debug
to Trace
and vice versa. Most of the recipes in this chapter use the Trace
class; to modify them so that they use the Debug
class instead, simply replace each instance of Trace
with Debug
in the code.
Catching and rethrowing exceptions is appropriate if you have a section of code where you want to perform some action if an exception occurs, but not perform any actions to actually handle the exception. To get the exception so that you can perform the initial action on it, establish a catch
block to catch the exception. Then, once the action has been performed, rethrow the exception from the catch
block in which the original exception was handled. Use the throw
keyword, followed by a semicolon, to rethrow an exception:
try { Console.WriteLine("In try"); int z2 = 9999999; checked { z2 *= 999999999; } } catch (OverflowException oe) { // Record the fact that the overflow exception occurred. EventLog.WriteEntry("MyApplication", oe.Message, EventLogEntryType.Error); throw; }
Here, you create an EventLog
entry that records the occurrence of an overflow exception. Then the exception is propagated up the call stack by the throw
statement.
Establishing a catch
block for an exception is essentially saying that you want to do something about that exceptional case.
If you do not rethrow the exception, or create a new exception to wrap the original exception and throw it, the assumption is that you have handled the condition that caused the exception and that the program can continue normal operation.
By choosing to rethrow the exception, you are indicating that there is still an issue to be dealt with and that you are counting on code farther up the stack to handle the condition. If you need to perform an action based on a thrown exception and need to allow the exception to continue after your code executes, then rethrowing is the mechanism to handle this. If both of those conditions are not met, don’t rethrow the exception; just handle it or remove the catch
block.
Remember that throwing exceptions is expensive. Try not to needlessly throw and rethrow exceptions, because this might bog down your application.
When rethrowing an exception, use throw;
instead of throw ex;
as the former will preserve the original call stack of the exception. Using throw
with the catch
parameter will reset the call stack to that location, and information about the error will be lost. There might be some scenarios where you want the call stack changed (to hide details of the internals of a portion of your application that performs sensitive operations, for example) but on the whole, give yourself the best chance to debug things and don’t truncate the call stack.
The real exception and its information can be obtained through the InnerException
property of the TargetInvocationException
that is thrown by MethodInfo.Invoke
.
Example 5-1 handles an exception that occurs within a method invoked via reflection. The Reflect
class contains a ReflectionException
method that invokes the static TestInvoke
method using the reflection classes.
using System; using System.Reflection; public static class Reflect { public static void ReflectionException() { Type reflectedClass = typeof(DebuggingAndExceptionHandling); try { MethodInfo methodToInvoke = reflectedClass.GetMethod("TestInvoke"); methodToInvoke?.Invoke(null, null); } catch(Exception e) { Console.WriteLine(e.ToShortDisplayString()); } } public static void TestInvoke() { throw (new Exception("Thrown from invoked method.")); } }
This code displays the following text:
Message: Exception has been thrown by the target of an invocation. Type: System.Reflection.TargetInvocationException Source: mscorlib TargetSite: System.Object InvokeMethod(System.Object, System.Object[], System.Si gnature, Boolean) **** INNEREXCEPTION START **** Message: Thrown from invoked method. Type: System.Exception Source: CSharpRecipes TargetSite: Void TestInvoke() **** INNEREXCEPTION END ****
When the methodToInvoke?.Invoke
method is called, the TestInvoke
method is called and throws an exception. The question mark next to methodToInvoke
is a null
-conditional operator to handle the case where the MethodInfo
could not be retrieved and is null
. This way, we didn’t have to write the check for null
around the invocation. The outer exception is the TargetInvocationException
; this is the generic exception thrown when a method invoked through reflection throws an exception. The CLR automatically wraps the original exception thrown by the invoked method inside of the TargetInvocationException
object’s InnerException
property. In this case, the exception thrown by the invoked method is of type System.Exception
. This exception is shown after the section that begins with the text **** INNEREXCEPTION START ****
.
To display the exception information, we call the ToShortDisplayString
method:
Console.WriteLine(e.ToShortDisplayString());
The ToShortDisplayString
extension method for Exception
uses a StringBuilder
to create the string of information about the exception and all inner exceptions. The WriteExceptionShortDetail
method populates the StringBuilder
with specific parts of the exception data. To get the inner exceptions, we use the GetNestedExceptionList
extension method:
public static string ToShortDisplayString(this Exception ex) { StringBuilder displayText = new StringBuilder(); WriteExceptionShortDetail(displayText, ex); foreach(Exception inner in ex.GetNestedExceptionList()) { displayText.AppendFormat("**** INNEREXCEPTION START ****{0}", Environment.NewLine); WriteExceptionShortDetail(displayText, inner); displayText.AppendFormat("**** INNEREXCEPTION END ****{0}{0}", Environment.NewLine); } return displayText.ToString(); } public static IEnumerable<Exception> GetNestedExceptionList( this Exception exception) { Exception current = exception; do { current = current.InnerException; if (current != null) yield return current; } while (current != null); } public static void WriteExceptionShortDetail(StringBuilder builder, Exception ex) { builder.AppendFormat("Message: {0}{1}", ex.Message, Environment.NewLine); builder.AppendFormat("Type: {0}{1}";, ex.GetType(), Environment.NewLine); builder.AppendFormat("Source: {0}{1}", ex.Source, Environment.NewLine); builder.AppendFormat("TargetSite: {0}{1}", ex.TargetSite, Environment.NewLine); }
The “Type Class,” “Null-Conditional Operator,” and “MethodInfo Class” topics in the MSDN documentation.
None of the built-in exceptions in the .NET Framework provide the implementation details that you require for an exception that you need to throw. You need to create your own exception class that operates seamlessly with your application, as well as other applications. Whenever an application receives this new exception, it can inform the user that a specific error occurred in a specific component. This report will greatly reduce the time required to debug the problem.
Create your own exception class. To illustrate, let’s create a custom exception class, RemoteComponentException
, that will inform a client application that an error has occurred in a remote server assembly.
The exception hierarchy starts with the Exception
class; from this are derived two classes: ApplicationException
and SystemException
. The SystemException
class and any classes derived from it are reserved for the developers of the FCL. Most of the common exceptions, such as the NullReferenceException
or the OverflowException
, are derived from SystemException
. The FCL developers created the ApplicationException
class for other developers using the .NET languages to derive their own exceptions from. This partitioning allows for a clear distinction between user-defined exceptions and the built-in system exceptions. However, Microsoft now recommends deriving directly from Exception
, rather than ApplicationException
. Nothing actively prevents you from deriving a class from either SystemException
or ApplicationException
. But it is better to be consistent and use the convention of always deriving from the Exception
class for user-defined exceptions.
You should follow the naming convention for exceptions when determining the name of your exception. The convention is very simple: decide on the exception’s name, and add the word Exception
to the end of it (e.g., use UnknownException
as the exception name instead of just Unknown
).
Every user-defined exception should include at least three constructors, which are described next. This is not a requirement, but it makes your exception classes operate similarly to every other exception class in the FCL and minimizes the learning curve for other developers using your new exception. These three constructors are:
Message
field of this exception. Like the default constructor, this constructor also calls the base class’s constructor, which also accepts a message string as its only parameter.innerException
parameter is added to the InnerException
property of this exception object. Like the other two constructors, this constructor calls the base class’s constructor of the same signature.Fields and their accessors should be created to hold data specific to the exception. Since this exception will be thrown as a result of an error that occurs in a remote server assembly, you will add a private field to contain the name of the server or service. In addition, you will add a public read-only property to access this field. Since you’re adding this new field, you should add two constructors that accept an extra parameter used to set the value of the serverName
field.
If necessary, override any base class members whose behavior is inherited by the custom exception class. For example, since you have added a new field, you need to determine whether it will need to be added to the default contents of the Message
field for this exception. If it does, you must override the Message
property:
public override string Message => $"{base.Message}{Environment.NewLine}" + $"The server ({this.ServerName ?? "Unknown"})" + "has encountered an error.";
Notice that the Message
property in the base class is displayed on the first line, and your additional text is displayed on the next line. This organization takes into account that a user might modify the message that will appear in the Message
property by using one of the overloaded constructors that takes a message string as a parameter.
Your exception object should be serializable and deserializable. This involves performing the following two additional steps:
Add the Serializable
attribute to the class definition. This attribute specifies that this class can be serialized and deserialized. A SerializationException
is thrown if this attribute does not exist on this class, and an attempt is made to serialize this class.
The class should implement the ISerializable
interface if you want control over how serialization and deserialization are performed, and it should provide an implementation for its single member, GetObjectData
. Here you implement it because the base class implements it, which means that you have no choice but to reimplement it if you want the fields you added (e.g., serverName
) to get serialized:
// Used during serialization to capture information about extra fields public override void GetObjectData(SerializationInfo exceptionInfo, StreamingContext exceptionContext) { base.GetObjectData(exceptionInfo, exceptionContext); exceptionInfo.AddValue("ServerName", this.ServerName); }
In addition, we need a new overridden constructor that accepts information to deserialize this object:
// Serialization ctor protected RemoteComponentException(SerializationInfo exceptionInfo, StreamingContext exceptionContext) : base(exceptionInfo, exceptionContext) { this.serverName = exceptionInfo.GetString("ServerName"); }
Even though it is not required, you should make all user-defined exception classes serializable and deserializable. That way, the exceptions can be propagated properly over remoting and application domain boundaries.
For the case where this exception will be caught in unmanaged code, such as a COM object, you can also set the HRESULT
value for this exception. An exception caught in unmanaged code becomes an HRESULT
value. If the exception does not alter the HRESULT
value, it defaults to the HRESULT
of the base class exception, which, in the case of a user-defined exception object that inherits from ApplicationException
, is COR_E_APPLICATION
(0x80131600
). To change the default HRESULT
value, simply set the value of this field in the constructor. The following code demonstrates this technique:
public class RemoteComponentException : Exception { public RemoteComponentException() : base() { HResult = 0x80040321; } public RemoteComponentException(string message) : base(message) { HResult = 0x80040321; } public RemoteComponentException(string message, Exception innerException) : base(message, innerException) { HResult = 0x80040321; } }
Now the HResult
that the COM object will see is the value 0x80040321
.
It is usually a good idea to override the Message
property in order to incorporate any new fields into the exception’s message text. Always remember to include the base class’s message text along with any additional text you add to this property.
At this point, the RemoteComponentException
class contains everything you need for a complete user-defined exception class.
As a final note, it is generally a good idea to place all user-defined exceptions in a separate assembly, which allows for easier reuse of these exceptions in other applications and, more importantly, allows other application domains and remotely executing code to both throw and handle these exceptions correctly no matter where they are thrown. The assembly that holds these exceptions should be signed with a strong name and added to the Global Assembly Cache (GAC), so that any code that uses or handles these exceptions can find the assembly that defines them. See Recipe 11.7 for more information on how to do this.
If you are sure that the exceptions being defined won’t ever be thrown or handled outside of your assembly, then you can leave the exception definitions there. But if for some reason an exception that you throw finds its way out of your assembly, the code that ultimately catches it will not be able to resolve it.
The complete source code for the RemoteComponentException
class is shown in Example 5-2.
using System; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Security.Permissions; [Serializable] public class RemoteComponentException : Exception, ISerializable { #region Constructors // Normal exception ctor's public RemoteComponentException() : base() { } public RemoteComponentException(string message) : base(message) { } public RemoteComponentException(string message, Exception innerException) : base(message, innerException) { } // Exception ctor's that accept the new ServerName parameter public RemoteComponentException(string message, string serverName) : base(message) { this.ServerName = serverName; } public RemoteComponentException(string message, Exception innerException, string serverName) : base(message, innerException) { this.ServerName = serverName; } // Serialization ctor protected RemoteComponentException(SerializationInfo exceptionInfo, StreamingContext exceptionContext) : base(exceptionInfo, exceptionContext) { this.ServerName = exceptionInfo.GetString("ServerName"); } #endregion // Constructors #region Properties // Read-only property for server name public string ServerName { get; } public override string Message => $"{base.Message}{Environment.NewLine}" + $"The server ({this.ServerName ?? "Unknown"})" + "has encountered an error."; #endregion // Properties #region Overridden methods // ToString method public override string ToString() => "An error has occurred in a server component of this client." + $"{Environment.NewLine}Server Name: " + $"{this.ServerName}{Environment.NewLine}" + $"{this.ToFullDisplayString()}"; // Used during serialization to capture information about extra fields [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("ServerName", this.ServerName); } #endregion // Overridden methods public string ToBaseString() => (base.ToString()); }
The ToFullDisplayString
call made in the ToString
override is an extension method for Exception
, with the GetNestedExceptionList
extension method used to get the list of exceptions and the WriteExceptionDetail
method to handle each Exception
’s details:
public static string ToFullDisplayString(this Exception ex) { StringBuilder displayText = new StringBuilder(); WriteExceptionDetail(displayText, ex); foreach (Exception inner in ex.GetNestedExceptionList()) { displayText.AppendFormat("**** INNEREXCEPTION START ****{0}", Environment.NewLine); WriteExceptionDetail(displayText, inner); displayText.AppendFormat("**** INNEREXCEPTION END ****{0}{0}", Environment.NewLine); } return displayText.ToString(); } public static IEnumerable<Exception> GetNestedExceptionList( this Exception exception) { Exception current = exception; do { current = current.InnerException; if (current != null) yield return current; } while (current != null); } public static void WriteExceptionDetail(StringBuilder builder, Exception ex) { builder.AppendFormat("Message: {0}{1}", ex.Message, Environment.NewLine); builder.AppendFormat("Type: {0}{1}", ex.GetType(), Environment.NewLine); builder.AppendFormat("HelpLink: {0}{1}", ex.HelpLink, Environment.NewLine); builder.AppendFormat("Source: {0}{1}", ex.Source, Environment.NewLine); builder.AppendFormat("TargetSite: {0}{1}", ex.TargetSite, Environment.NewLine); builder.AppendFormat("Data:{0}", Environment.NewLine); foreach (DictionaryEntry de in ex.Data) { builder.AppendFormat(" {0} : {1}{2}", de.Key, de.Value, Environment.NewLine); } builder.AppendFormat("StackTrace: {0}{1}", ex.StackTrace, Environment.NewLine); }
A partial listing of the code to test the RemoteComponentException
class is shown in Example 5-3.
public void TestSpecializedException() { // Generic inner exception used to test the // RemoteComponentException's inner exception. Exception inner = new Exception("The inner Exception"); RemoteComponentException se1 = new RemoteComponentException (); RemoteComponentException se2 = new RemoteComponentException ("A Test Message for se2"); RemoteComponentException se3 = new RemoteComponentException ("A Test Message for se3", inner); RemoteComponentException se4 = new RemoteComponentException ("A Test Message for se4", "MyServer"); RemoteComponentException se5 = new RemoteComponentException ("A Test Message for se5", inner, "MyServer"); // Test overridden Message property. Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- MESSAGE PROPERTY"); Console.WriteLine("se1.Message == " + se1.Message); Console.WriteLine("se2.Message == " + se2.Message); Console.WriteLine("se3.Message == " + se3.Message); Console.WriteLine("se4.Message == " + se4.Message); Console.WriteLine("se5.Message == " + se5.Message); // Test -overridden- ToString method. Console.WriteLine(Environment.NewLine + "TEST -OVERRIDDEN- TOSTRING METHOD"); Console.WriteLine("se1.ToString() == " + se1.ToString()); Console.WriteLine("se2.ToString() == " + se2.ToString()); Console.WriteLine("se3.ToString() == " + se3.ToString()); Console.WriteLine("se4.ToString() == " + se4.ToString()); Console.WriteLine("se5.ToString() == " + se5.ToString()); Console.WriteLine(Environment.NewLine + "END TEST" + Environment.NewLine); }
The output from Example 5-3 is presented in Example 5-4.
TEST -OVERRIDDEN- MESSAGE PROPERTY se1.Message == Exception of type 'CSharpRecipes.ExceptionHandling+RemoteComponen tException' was thrown. A server with an unknown name has encountered an error. se2.Message == A Test Message for se2 A server with an unknown name has encountered an error. se3.Message == A Test Message for se3 A server with an unknown name has encountered an error. se4.Message == A Test Message for se4 The server (MyServer) has encountered an error. se5.Message == A Test Message for se5 The server (MyServer) has encountered an error. TEST -OVERRIDDEN- TOSTRING METHOD se1.ToString() == An error has occurred in a server component of this client. Server Name: Message: Exception of type 'CSharpRecipes.ExceptionHandling+RemoteComponentExcep tion' was thrown. A server with an unknown name has encountered an error. Type: CSharpRecipes.ExceptionHandling+RemoteComponentException HelpLink: Source: TargetSite: Data: StackTrace: se2.ToString() == An error has occurred in a server component of this client. Server Name: Message: A Test Message for se2 A server with an unknown name has encountered an error. Type: CSharpRecipes.ExceptionHandling+RemoteComponentException HelpLink: Source: TargetSite: Data: StackTrace: se3.ToString() == An error has occurred in a server component of this client. Server Name: Message: A Test Message for se3 A server with an unknown name has encountered an error. Type: CSharpRecipes.ExceptionHandling+RemoteComponentException HelpLink: Source: TargetSite: Data: StackTrace: **** INNEREXCEPTION START **** Message: The Inner Exception Type: System.Exception HelpLink: Source: TargetSite: Data: StackTrace: **** INNEREXCEPTION END **** se4.ToString() == An error has occurred in a server component of this client. Server Name: MyServer Message: A Test Message for se4 The server (MyServer) has encountered an error. Type: CSharpRecipes.ExceptionHandling+RemoteComponentException HelpLink: Source: TargetSite: Data: StackTrace: se5.ToString() == An error has occurred in a server component of this client. Server Name: MyServer Message: A Test Message for se5 The server (MyServer) has encountered an error. Type: CSharpRecipes.ExceptionHandling+RemoteComponentException HelpLink: Source: TargetSite: Data: StackTrace: **** INNEREXCEPTION START **** Message: The Inner Exception Type: System.Exception HelpLink: Source: TargetSite: Data: StackTrace: **** INNEREXCEPTION END **** END TEST
Recipe 11.7, and the “Using User-Defined Exceptions” and “Exception Class” topics in the MSDN documentation.
You need to fix a problem with your code that is throwing an exception. Unfortunately, an exception handler is trapping the exception, and you are having a tough time pinpointing where and when the exception is being thrown.
Forcing the application to break on an exception before the application has a chance to handle it is very useful in situations in which you need to step through the code at the point where the exception is first being thrown. If this exception were thrown and not handled by your application, the debugger would intervene and break on the line of code that caused the unhandled exception. In this case, you can see the context in which the exception was thrown. However, if an exception handler is active when the exception is thrown, the exception handler will handle it and continue on, preventing you from being able to see the context at the point where the exception was thrown. This is the default behavior for all exceptions.
Select Debug→Exceptions or press Ctrl-Alt-E within Visual Studio 2015 to display the Exception Settings tool window (see Figure 5-1). Select the exception from the tree that you want to modify and then click on the checkbox in the tree view. Click OK and then run your application. Any time the application throws a System.ArgumentOutOfRangeException
, the debugger will break on that line of code before your application has a chance to handle it.
Using the Exception Settings tool window, you can target specific exceptions or sets of exceptions for which you wish to alter the default behavior. This dialog has three main sections. The first is the TreeView control, which contains the list of categorized exceptions. Using this TreeView, you can choose one or more exceptions or groups of exceptions whose behavior you wish to modify.
The next section on this dialog is the column Thrown in the list next to the TreeView. This column contains a checkbox for each exception that will enable the debugger to break when that type of exception is first thrown. At this stage, the exception is considered a first-chance exception. Checking the checkbox in the Thrown column forces the debugger to intervene when a first-chance exception of the type chosen in the TreeView control is thrown. Unchecking the checkbox allows the application to attempt to handle the first-chance exception.
You can also click on the Filter icon in the top left of the window in order to narrow down the view of the exceptions to just the ones you have selected to break on a first-chance exception, as shown in Figure 5-2.
The Exception Settings tool window also provides a Search bar at the top to allow you to search for exceptions in the window. If you type argumentnullexception
in the window, you will see the selection narrow to just items that match that text, as shown in Figure 5-3.
To add a user-defined exception to the Exception Settings, click the Add button. You’ll see the dialog box shown in Figure 5-4.
Press Yes to use the original Exceptions dialog, which is shown in Figure 5-5.
This dialog contains two helpful buttons, Find and Find Next, to allow you to search for an exception rather than dig into the TreeView control and search for it on your own. In addition, three other buttons—Reset All, Add, and Delete—allow you to reset to the original state and to add and remove user-defined exceptions, respectively.
For example, you can create your own exception, as you did in Recipe 5.3, and add this exception to the TreeView list. You must add any managed exception such as this to the TreeView node entitled Common Language Runtime Exceptions. This setting tells the debugger that this is a managed exception and should be handled as such. Figure 5-6 shows the addition of the custom exception.
Type the name of the exception—exactly as its class name is spelled with the full namespace scoping into the Name
field of this dialog box. Do not append any other information to this name, such as the namespace it resides in or a class name that it is nested within. Doing so will prevent the debugger from seeing this exception when it is thrown. Clicking the OK button places this exception into the TreeView under the Common Language Runtime Exceptions node. The Exceptions dialog box will look something like the one in Figure 5-7 after you add this user-defined exception.
The Delete button deletes any selected user-defined exception that you added to the TreeView. The Reset All button deletes any and all user-defined exceptions that have been added to the TreeView. Check the Thrown column to have the debugger stop when that exception type is thrown.
There is one other setting that can affect your exception debugging: Just My Code (Figure 5-8). You should turn this off to get the best picture of what is really happening in your application when debugging; when it is enabled, you cannot see the related actions of the framework code that your code calls. Being able to see where your code calls into the framework and where it goes from there is very educational and can help you understand the issue you are debugging better. The setting is under ToolsOptionsDebuggingGeneral in Visual Studio 2015.
The “Exception Handling (Debugging)” topic in the MSDN documentation.
Wrap the EndInvoke
method of the delegate in a try
-catch
block:
using System; using System.Threading; public class AsyncAction { public void PollAsyncDelegate() { // Create the async delegate to call Method1 and call its // BeginInvokemethod. AsyncInvoke MI = new AsyncInvoke(TestAsyncInvoke.Method1); IAsyncResult AR = MI.BeginInvoke(null, null); // Poll until the async delegate is finished. while (!AR.IsCompleted) { System.Threading.Thread.Sleep(100); Console.Write('.'), } Console.WriteLine("Finished Polling"); // Call the EndInvoke method of the async delegate. try { int RetVal = MI.EndInvoke(AR); Console.WriteLine("RetVal (Polling): " + RetVal); } catch (Exception e) { Console.WriteLine(e.ToString()); } } }
The following code defines the AsyncInvoke
delegate and the asynchronously invoked static method TestAsyncInvoke.Method1
:
public delegate int AsyncInvoke(); public class TestAsyncInvoke { public static int Method1() { throw (new Exception("Method1")); // Simulate an exception being thrown. } }
If the code in the PollAsyncDelegate
method did not contain a call to the delegate’s EndInvoke
method, the exception thrown in Method1
either would simply be discarded and never caught or, if the application had the top-level exception handlers wired up (Recipes 5.2, 5.7, and 5.8), it would be caught. If EndInvoke
is called, then this exception would occur when EndInvoke
is called and could be caught there. This behavior is by design; for all unhandled exceptions that occur within the thread, the thread immediately returns to the thread pool, and the exception is lost.
If a method that was called asynchronously through a delegate throws an exception, the only way to trap that exception is to include a call to the delegate’s EndInvoke
method and wrap this call in an exception handler. You must call the EndInvoke
method to retrieve the results of the asynchronous delegate; in fact, you must call it even if there are no results. You can obtain these results through a return value or any ref
or out
parameters of the delegate.
Use the Data
property on the System.Exception
object to store key/value pairs of information relevant to the exception.
For example, say there is a System.ArgumentException
being thrown from a section of code, and you want to include the underlying cause and the length of time it took. You would add two key/value pairs to the Exception.Data
property by specifying the key in the indexer and then assigning the value.
In the example that follows, the Data
for the irritable
exception uses "Cause"
and "Length"
for its keys. Once the items have been set in the Data
collection, the exception can be thrown and caught, and more data can be added in subsequent catch
blocks for as many levels of exception handling as the exception is allowed to traverse:
try { try { try { try { ArgumentException irritable = new ArgumentException("I'm irritable!"); irritable.Data["Cause"]="Computer crashed"; irritable.Data["Length"]=10; throw irritable; } catch (Exception e) { // See if I can help... if(e.Data.Contains("Cause")) e.Data["Cause"]="Fixed computer" throw; } } catch (Exception e) { e.Data["Comment"]="Always grumpy you are"; throw; } } catch (Exception e) { e.Data["Reassurance"]="Error Handled"; throw; } }
The final catch
block can then iterate over the Exception.Data
collection and display all of the supporting data that has been gathered in the Data
collection since the initial exception was thrown:
catch (Exception e) { Console.WriteLine("Exception supporting data:"); foreach(DictionaryEntry de in e.Data) { Console.WriteLine(" {0} : {1}",de.Key,de.Value); } }
Exception.Data
is an object that supports the IDictionary
interface. This allows you to:
Add and remove name/value pairs.
Clear the contents.
Search the collection to see if it contains a certain key.
Get an IDictionaryEnumerator
for rolling over the collection items.
Index into the collection using the key.
Access an ICollection
of all of the keys and all of the values separately.
Items placed into Exception.Data
need to be Serializable
or they will throw an ArgumentException
on the addition to the collection. If you are adding a class to Exception.Data
, mark it as Serializable
and make sure it can be serialized.
public void TestExceptionDataSerializable() { Exception badMonkey = new Exception("You are a bad monkey!"); try { badMonkey.Data["Details"] = new Monkey(); } catch (ArgumentException aex) { Console.WriteLine(aex.Message); } } //[Serializable] // Uncomment to make serializable and work public class Monkey { public string Name { get; } = "George"; }
It is very handy to be able to tack on code-specific data to the system exceptions, as it allows you to give a more complete picture of what happened in the code when the error occurred. The more information available to the poor soul (probably yourself) who is trying to figure out why the exception was thrown in the first place, the better the chance of it being fixed. Do yourself and your team a favor and give a little bit of extra information when throwing exceptions; you won’t be sorry you did.
The “Exception.Data Property” topic in the MSDN documentation.
You need to hook up handlers for both the System.Windows.Forms.Application. ThreadException
event and the System.Appdomain.UnhandledException
event. Both of these events need to be hooked up, as the WinForms support in the Framework does a lot of exception trapping itself. It exposes the System.Windows.Forms.Application.ThreadException
event to allow you to get any unhandled exceptions that happen on the UI thread that the WinForms and their events are running on. In spite of its deceptive name, the System.Windows.Forms.Application.ThreadException
event handler will not catch unhandled exceptions on worker threads constructed by the program or from ThreadPool
threads. In order to catch all of those possible routes for unhandled exceptions in a WinForms application, you need to hook up a handler for the System.AppDomain.UnhandledException
event (System.Windows.Forms.Application.ThreadException
will catch UI thread exceptions).
To hook up the necessary event handlers to catch all of your unhandled exceptions in a WinForms application, add the following code to the Main
function in your application:
static void Main() { // Adds the event handler to catch any exceptions that happen // in the main UI thread. Application.ThreadException += new ThreadExceptionEventHandler(OnThreadException); // Add the event handler for all threads in the appdomain except // for the main UI thread. appdomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); Application.EnableVisualStyles(); Application.Run(new Form1()); }
The System.AppDomain.UnhandledException
event handler is hooked up to the current Appdomain
via the appdomain.CurrentDomain
property, which gives access to the current Appdomain
. The ThreadException
handler for the application is accessed through the Application.ThreadException
property.
The event handler code is established in the CurrentDomain_UnhandledException
and OnThreadException
handler methods. See Recipe 5.8 for more information on the UnhandledExceptionEventHandler
. The ThreadExceptionEventHandler
is passed the sender object and a ThreadExceptionEventArgs
object. ThreadExceptionEventArgs
has an Exception
property that contains the unhandled exception from the WinForms UI thread:
// Handles the exception event for all other threads static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { // Just show the exception details. MessageBox.Show("CurrentDomain_UnhandledException: " + e.ExceptionObject.ToString()); } // Handles the exception event from a UI thread static void OnThreadException(object sender, ThreadExceptionEventArgs t) { // Just show the exception details. MessageBox.Show("OnThreadException: " + t.Exception.ToString()); }
Exceptions are the primary way to convey errors in .NET, so when you build an application, it is imperative that there be a final line of defense against unhandled exceptions. An unhandled exception will crash the program (even if it looks a bit nicer in .NET); this is not the impression you wish to make on your customers. It would have been nice to have one event to hook up to for all unhandled exceptions. The appdomain.UnhandledException
event comes pretty close to that, but having to handle one extra event isn’t the end of the world, either. In coding event handlers for both appdomain.UnhandledException
and Application.ThreadException
, you can easily call a single handler that writes the exception information to the event log, the debug stream, or custom trace logs or even sends you an email with the information. The possibilities are limited only by how you want to handle errors that can happen to any program given enough exposure.
The “ThreadExceptionEventHandler Delegate” and “UnhandledExceptionEventHandler Delegate” topics in the MSDN documentation.
To hook up the necessary event handlers to catch all of your unhandled exceptions in a WPF application, add the following code to the App.xaml file in your application:
<Application x:Class="UnhandledWPFException.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml" DispatcherUnhandledException="Application_DispatcherUnhandledException"> <Application.MainWindow> <Window /> </Application.MainWindow> <Application.Resources> </Application.Resources> </Application>
Then, in the codebehind file App.xaml.cs, add the Application_DispatcherUnhandledException
method to handle otherwise unhandled exceptions:
private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { // Log the exception information in the event log EventLog.WriteEntry("UnhandledWPFException Application", e.Exception.ToString(), EventLogEntryType.Error); // Let the user know what happenned MessageBox.Show("Application_DispatcherUnhandledException: " + e.Exception.ToString()); // indicate we handled it e.Handled = true; // shut down the application this.Shutdown(); }
Windows Presentation Foundation provides another way to create Windows-based applications for .NET. Protecting users from unsightly unhandled exceptions requires a bit of code in WPF, just as it does in WinForms (see Recipe 5.7 for doing this in WinForms).
The System.Windows.Application
class is the base class for WPF-based applications, and it is from here that the unhandled exceptions are handled via the DispatcherUnhandledException
event. You set up this event handler by specifying the method to handle the event in the App.xaml file shown here:
DispatcherUnhandledException="Application_DispatcherUnhandledException">
You can also set this up in code directly instead of doing it the XAML way by adding the Startup
event handler (which is where Microsoft recommends you put the initialization code for the application in WPF) to the XAML file like this:
<Application x:Class="UnhandledWPFException.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml" Startup="Application_Startup" > <Application.MainWindow> <Window /> </Application.MainWindow> <Application.Resources> </Application.Resources> </Application>
In the Startup
event, establish the event handler for the DispatcherUnhandledException
like this:
private void Application_Startup(object sender, StartupEventArgs e) { this.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler( Application_DispatcherUnhandledException); }
This is great for handling exceptions for WPF applications: just hook up and get all those unhandled exceptions delivered to your single handler, right? Wrong. Just as was necessary in WinForms applications, if you have any code running on any threads other than the UI thread (which you almost always will), you still have to hook up to the AppDomain
for the AppDomain.UnhandledException
handler to catch those exceptions on threads other than the UI thread. In order to do that, we update our App.xaml.cs file as follows:
/// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App : Application { private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { // indicate we handled it e.Handled = true; ReportUnhandledException(e.Exception); } private void Application_Startup(object sender, StartupEventArgs e) { // WPF UI exceptions this.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler( Application_DispatcherUnhandledException); // Those dirty thread exceptions AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { ReportUnhandledException(e.ExceptionObject as Exception); } private void ReportUnhandledException(Exception ex) { // Log the exception information in the event log EventLog.WriteEntry("UnhandledWPFException Application", ex.ToString(), EventLogEntryType.Error); // Let the user know what happenned MessageBox.Show("Unhandled Exception: " + ex.ToString()); // shut down the application this.Shutdown(); } }
Recipe 5.7; the “DispatcherUnhandledException event” and “AppDomain. UnhandledException handler” topics in the MSDN documentation.
Use the GetProcessState
method and ProcessRespondingState
enumeration shown in Example 5-5 to determine whether a process has stopped responding.
public enum ProcessRespondingState { Responding, NotResponding, Unknown } public static ProcessRespondingState GetProcessState(Process p) { if (p.MainWindowHandle == IntPtr.Zero) { Trace.WriteLine($"{p.ProcessName} does not have a MainWindowHandle"); return ProcessRespondingState.Unknown; } else { // This process has a MainWindowHandle if (!p.Responding) return ProcessRespondingState.NotResponding; else return ProcessRespondingState.Responding; } }
The GetProcessState
method accepts a single parameter, process
, identifying a process. The Responding
property is then called on the Process
object represented by the process
parameter. This property returns a ProcessRespondingState
enumeration value to indicate that a process is currently responding (Responding
), that it is not currently responding (NotResponding
), or that a response cannot be determined for this process as there is no main window handle (Unknown
).
The Responding
property always returns true
if the process in question does not have a MainWindowHandle
. Processes such as Idle, spoolsv, Rundll32, and svchost do not have a main window handle, and therefore the Responding
property always returns true
for them. To weed out these processes, you can use the MainWindowHandle
property of the Process
class, which returns the handle of the main window for a process. If this property returns 0
, the process has no main window.
To determine whether all processes on a machine are responding, you can call the GetProcessState
method as follows:
var processes = Process.GetProcesses().ToArray(); Array.ForEach(processes, p => { var processState = GetProcessState(p); switch (processState) { case ProcessRespondingState.NotResponding: Console.WriteLine($"{p.ProcessName} is not responding."); break; case ProcessRespondingState.Responding: Console.WriteLine($"{p.ProcessName} is responding."); break; case ProcessRespondingState.Unknown: Console.WriteLine( $"{p.ProcessName}'s state could not be determined."); break; } });
This code snippet iterates over all processes currently running on your system. The static GetProcesses
method of the Process
class takes no parameters and returns an array of Process
objects with information for all processes running on your system. Each Process
object is then passed in to your GetProcessState
method to determine whether it is responding. Other static methods on the Process
class that retrieve Process
objects are GetProcessById
, GetCurrentProcess
, and GetProcessesByName
.
The “Process Class” topic in the MSDN documentation.
You need to add the ability for your application to log events that occur in your application, such as startup, shutdown, critical errors, and even security breaches. Along with reading and writing to a log, you need the ability to create, clear, close, and remove logs from the event log.
Your application might need to keep track of several logs at one time. For example, your application might use a custom log to track specific events, such as startup and shutdown, as they occur in your application. To supplement the custom log, your application could make use of the security log already built into the event log system to read/write security events that occur in your application.
Support for multiple logs comes in handy when one log needs to be created and maintained on the local computer and another, duplicate log needs to be created and maintained on a remote machine. This remote machine might contain logs of all running instances of your application on each user’s machine. An administrator could use these logs to quickly find any problems that occur or discover if security is breached in your application. In fact, an application could be run in the background on the remote administrative machine that watches for specific log entries to be written to this log from any user’s machine. Recipe 13.6 uses an event mechanism to watch for entries written to an event log and could easily be used to enhance this recipe.
Use the event log built into the Microsoft Windows operating system to record specific events that occur infrequently.
Don’t flood the event log with many different entries that you could handle by enabling or disabling tracing. Errors are a must, followed by the very important items, but not everything should be written to the event log. Be judicious when writing to the event log so you don’t have to sort through all of it when you are looking for the clues.
The AppEvents
class shown in Example 5-6 contains all the methods needed to create and use an event log in your application.
using System;
using System.Diagnostics;
public class AppEvents
{
// If you encounter a SecurityException trying to read the registry
// (Security log) follow these instructions:
// 1) Open the Registry Editor (search for regedit or type regedit at the Run
// prompt) 2) Navigate to the following key:
// 3) HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesEventlogSecurity
// 4) Right-click on this entry and select Permissions
// 5) Add the user you are logged in as and give the user the Read permission
// If you encounter a SecurityException trying to write to the event log
// "Requested registry access is not allowed.", then the event source has not
// been created. Try re-running the EventLogInstaller for your custom event or
// for this sample code, run %WINDOWS%Microsoft.NETFrameworkv4.0.30319
// InstallUtil.exe AppEventsEventLogInstallerApp.dll"
// If you just ran it, you may need to wait a bit until Windows catches up and
// recognizes the log that was added.
const string localMachine = ".";
// Constructors
public AppEvents(string logName) :
this(logName, Process.GetCurrentProcess().ProcessName)
{ }
public AppEvents(string logName, string source) :
this(logName, source, localMachine)
{ }
public AppEvents(string logName, string source,
string machineName = localMachine)
{
this.LogName = logName;
this.SourceName = source;
this.MachineName = machineName;
Log = new EventLog(LogName, MachineName, SourceName);
}
private EventLog Log { get; set; } = null;
public string LogName { get; set; }
public string SourceName { get; set; }
public string MachineName { get; set; } = localMachine;
// Methods
public void WriteToLog(string message, EventLogEntryType type,
CategoryType category, EventIDType eventID)
{
if (Log == null)
throw (new ArgumentNullException(nameof(Log),
"This Event Log has not been opened or has been closed."));
EventLogPermission evtPermission =
new EventLogPermission(EventLogPermissionAccess.Write, MachineName);
evtPermission.Demand();
// If you get a SecurityException here, see the notes at the
// top of the class
Log.WriteEntry(message, type, (int)eventID, (short)category);
}
public void WriteToLog(string message, EventLogEntryType type,
CategoryType category, EventIDType eventID, byte[] rawData)
{
if (Log == null)
throw (new ArgumentNullException(nameof(Log),
"This Event Log has not been opened or has been closed."));
EventLogPermission evtPermission =
new EventLogPermission(EventLogPermissionAccess.Write, MachineName);
evtPermission.Demand();
// If you get a SecurityException here, see the notes at the
// top of the class
Log.WriteEntry(message, type, (int)eventID, (short)category, rawData);
}
public IEnumerable<EventLogEntry> GetEntries()
{
EventLogPermission evtPermission =
new EventLogPermission(EventLogPermissionAccess.Administer, MachineName);
evtPermission.Demand();
return Log?.Entries.Cast<EventLogEntry>().Where(evt =>
evt.Source == SourceName);
}
public void ClearLog()
{
EventLogPermission evtPermission =
new EventLogPermission(EventLogPermissionAccess.Administer, MachineName);
evtPermission.Demand();
if (!IsNonCustomLog())
Log?.Clear();
}
public void CloseLog()
{
Log?.Close();
Log = null;
}
public void DeleteLog()
{
if (!IsNonCustomLog())
if (EventLog.Exists(LogName, MachineName))
EventLog.Delete(LogName, MachineName);
CloseLog();
}
public bool IsNonCustomLog()
{
// Because Application, Setup, Security, System, and other non-custom logs
// can contain crucial information you can't just delete or clear them
if (LogName == string.Empty || // same as application
LogName == "Application" ||
LogName == "Security" ||
LogName == "Setup" ||
LogName == "System")
{
return true;
}
return false;
}
}
The EventIDType
and CategoryType
enumerations used in this class are defined as follows:
public enum EventIDType { NA = 0, Read = 1, Write = 2, ExceptionThrown = 3, BufferOverflowCondition = 4, SecurityFailure = 5, SecurityPotentiallyCompromised = 6 } public enum CategoryType : short { None = 0, WriteToDB = 1, ReadFromDB = 2, WriteToFile = 3, ReadFromFile = 4, AppStartUp = 5, AppShutDown = 6, UserInput =7 }
As a last note, the EventIDType
and CategoryType
enumerations are designed mainly to log security-type breaches as well as potential attacks on the security of your application. Using these event IDs and categories, the administrator can more easily track down potential security threats and do postmortem analysis after security is breached. You can easily modify or replace these enumerations with your own to track different events that occur as a result of your application running.
The AppEvents
class created for this recipe provides applications with an easy-to-use interface for creating, using, and deleting single or multiple event logs in your application. The methods of the AppEvents
class are described as follows:
WriteToLog
GetEntries
IEnumerable<EventLogEntry>
.ClearLog
CloseLog
DeleteLog
You can add an AppEvents
object to an array or collection containing other AppEvents
objects; each AppEvents
object corresponds to a particular event log. The following code creates two AppEvents
classes and adds them to a ListDictionary
collection:
public void CreateMultipleLogs() { AppEvents AppEventLog = new AppEvents("AppLog", "AppLocal"); AppEvents GlobalEventLog = new AppEvents("AppSystemLog", "AppGlobal"); ListDictionary LogList = new ListDictionary(); LogList.Add(AppEventLog.Name, AppEventLog); LogList.Add(GlobalEventLog.Name, GlobalEventLog);
To write to either of these two logs, obtain the AppEvents
object by name from the ListDictionary
object, cast the resultant object type to an AppEvents
type, and call the WriteToLog
method:
((AppEvents)LogList[AppEventLog.Name]).WriteToLog("App startup", EventLogEntryType.Information, CategoryType.AppStartUp, EventIDType.ExceptionThrown); ((AppEvents)LogList[GlobalEventLog.Name]).WriteToLog( "App startup security check", EventLogEntryType.Information, CategoryType.AppStartUp, EventIDType.BufferOverflowCondition);
Containing all AppEvents
objects in a ListDictionary
object allows you to easily iterate over all the AppEvents
that your application has instantiated. Using a foreach
loop, you can write a single message to both a local and a remote event log:
foreach (DictionaryEntry Log in LogList) { ((AppEvents)Log.Value).WriteToLog("App startup", EventLogEntryType.FailureAudit, CategoryType.AppStartUp, EventIDType.SecurityFailure); }
To delete each log in the logList
object, you can use the following foreach
loop:
foreach (DictionaryEntry Log in LogList) { ((AppEvents)Log.Value).DeleteLog(); } LogList.Clear();
You should be aware of several key points. The first concerns a small problem with constructing multiple AppEvents
classes. If you create two AppEvents
objects and pass in the same source
string to the AppEvents
constructor, an exception will be thrown. Consider the following code, which instantiates two AppEvents
objects with the same source
string:
AppEvents appEventLog = new AppEvents("AppLog", "AppLocal"); AppEvents globalEventLog = new AppEvents("Application", "AppLocal");
The objects are instantiated without errors, but when the WriteToLog
method is called on the globalEventLog
object, the following exception is thrown:
An unhandled exception of type 'System.ArgumentException' occurred in system.dll. Additional information: The source 'AppLocal' is not registered in log 'Application'. (It is registered in log 'AppLog'.) " The Source and Log properties must be matched, or you may set Log to the empty string, and it will automatically be matched to the Source property.
This exception occurs because the WriteToLog
method internally calls the WriteEntry
method of the EventLog
object. The WriteEntry
method internally checks to see whether the specified source is registered to the log you are attempting to write to. In this case, the AppLocal
source was registered to the first log it was assigned to—the AppLog
log. The second attempt to register this same source to another log, Application
, failed silently. You do not know that this attempt failed until you try to use the WriteEntry
method of the EventLog
object.
Another key point about the AppEvents
class is the following code, placed at the beginning of each method (except for the DeleteLog
method):
if (log == null) throw (new ArgumentNullException("log", "This Event Log has not been opened or has been closed."));
This code checks to see whether the private member variable log is a null
reference. If so, an ArgumentException
is thrown, informing the user of this class that a problem occurred with the creation of the EventLog
object. The DeleteLog
method does not check the log
variable for null
since it deletes the event log source and the event log itself. The EventLog
object is not involved in this process except at the end of this method, where the log is closed and set to null
, if it is not already null
. Regardless of the state of the log
variable, the source and event log should be deleted in this method.
The ClearLog
and DeleteLog
methods make a critical choice when determining whether to delete a log. The following code prevents the application, security, setup, and system event logs from being deleted from your system:
public bool IsNonCustomLog() { // Because Application, Setup, Security, System, and other non-custom logs // can contain crucial information you can't just delete or clear them if (LogName == string.Empty || // same as application LogName == "Application" || LogName == "Security" || LogName == "Setup" || LogName == "System") { return true; } return false; }
If any of these logs is deleted, so are the sources registered with the particular log. Once the log is deleted, it is permanent; believe us, it is not fun to try to re-create the log and its sources without a backup.
In order for the AppEvents
class to work, however, it first needs an event source created. The event log uses event sources to determine which application logged the event. You can establish an event source only when running in an administrative context, and there are two ways to accomplish this:
Call the EventLog.CreateEventSource
method.
Use an EventLogInstaller
.
While you could create a console application that calls the CreateEventSource
method and have a user run it in an administrative context on her machine, the recommended option is to build an EventLogInstaller
class that can be used with InstallUtil.exe (provided by the .NET Framework) to create your initial event sources and custom logs.
The AppEventsEventLogInstaller
, shown next, will establish the event logs and sources for us. This installer can be called not only by InstallUtil
but also by most major installation packages, so you can plug it into your favorite installation software to register your event logs and sources at install time when your users have administrative access (or at least the help of an IT professional with said access):
/// <summary> /// To INSTALL: C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.exe [PathToBinary]AppEventsEventLogInstallerApp.dll /// To UNINSTALL: C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.exe -u [PathToBinary]AppEventsEventLogInstallerApp.dll /// </summary> [RunInstaller(true)] public class AppEventsEventLogInstaller : Installer { private EventLogInstaller evtLogInstaller; public AppEventsEventLogInstaller() { evtLogInstaller = new EventLogInstaller(); evtLogInstaller.Source = "APPEVENTSSOURCE"; evtLogInstaller.Log = ""; // Default to Application Installers.Add(evtLogInstaller); evtLogInstaller = new EventLogInstaller(); evtLogInstaller.Source = "AppLocal"; evtLogInstaller.Log = "AppLog"; Installers.Add(evtLogInstaller); evtLogInstaller = new EventLogInstaller(); evtLogInstaller.Source = "AppGlobal"; evtLogInstaller.Log = "AppSystemLog"; Installers.Add(evtLogInstaller); } public static void Main() { AppEventsEventLogInstaller appEventsEventLogInstaller = new AppEventsEventLogInstaller(); } }
If you are using InstallUtil
to set this up locally, here is a sample of what you may see when installing using a proper administrative context (“Run As Administrator”):
C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.exe C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll C:WINDOWSsystem32>C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.ex e C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Microsoft (R) .NET Framework Installation utility Version 4.0.30319.33440 Copyright (C) Microsoft Corporation. All rights reserved. Running a transacted installation. Beginning the Install phase of the installation. See the contents of the log file for the C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll assembly's progress. The file is located at C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog. Installing assembly 'C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll'. Affected parameters are: logtoconsole = logfile = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog assemblypath = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Creating EventLog source APPEVENTSSOURCE in log ... Creating EventLog source AppLocal in log AppLog... Creating EventLog source AppGlobal in log AppSystemLog... The Install phase completed successfully, and the Commit phase is beginning. See the contents of the log file for the C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll assembly's progress. The file is located at C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog. Committing assembly 'C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll'. Affected parameters are: logtoconsole = logfile = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog assemblypath = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll The Commit phase completed successfully. The transacted install has completed.
If you attempt to run InstallUtil
in a nonadministrative context, you will see results similar to the following:
C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.exe C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Microsoft (R) .NET Framework Installation utility Version 4.0.30319.33440 Copyright (C) Microsoft Corporation. All rights reserved. Running a transacted installation. Beginning the Install phase of the installation. See the contents of the log file for the C:CSCB6AppEventsEventLogInstallerAppinDebugAppEventsEventLogInstallerApp.dll assembly's progress. The file is located at C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog. Installing assembly 'C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll'. Affected parameters are: logtoconsole = logfile = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog assemblypath = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Creating EventLog source APPEVENTSSOURCE in log APPEVENTSLOG... An exception occurred during the Install phase. System.Security.SecurityException: The source was not found, but some or all event logs could not be searched. Inaccessible logs: Security. The Rollback phase of the installation is beginning. See the contents of the log file for the C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll assembly's progress. The file is located at C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog. Rolling back assembly 'C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll'. Affected parameters are: logtoconsole = logfile = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog assemblypath = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Restoring event log to previous state for source APPEVENTSSOURCE. An exception occurred during the Rollback phase of the System.Diagnostics.EventLogInstaller installer. System.Security.SecurityException: Requested registry access is not allowed. An exception occurred during the Rollback phase of the installation. This except ion will be ignored and the rollback will continue. However, the machine might n ot fully revert to its initial state after the rollback is complete. The Rollback phase completed successfully. The transacted install has completed. The installation failed, and the rollback has been performed.
Not only can InstallUtil
install your event logs and sources, it can help remove them too using the –u
parameter!
C:WindowsMicrosoft.NETFrameworkv4.0.30319InstallUtil.exe -u C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Microsoft (R) .NET Framework Installation utility Version 4.0.30319.33440 Copyright (C) Microsoft Corporation. All rights reserved. The uninstall is beginning. See the contents of the log file for the C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll assembly's progress. The file is located at C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog. Uninstalling assembly 'C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll'. Affected parameters are: logtoconsole = logfile = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.InstallLog assemblypath = C:CSCB6AppEventsEventLogInstallerAppinDebug AppEventsEventLogInstallerApp.dll Removing EventLog source AppGlobal. Deleting event log AppSystemLog. Removing EventLog source AppLocal. Deleting event log AppLog. Removing EventLog source APPEVENTSSOURCE. The uninstall has completed.
You should minimize the number of entries written to the event log from your application, as writing to the event log causes a performance hit and some logs are not set to roll over or clear after a certain number of entries. Writing too much information to the event log can noticeably slow your application or cause the server problems. Choose the entries you write to the event log wisely.
The “EventLog Class,” “InstallUtil.exe,” and “EventLogInstaller Class” topics in the MSDN documentation.
You may have multiple applications that write to a single event log. For each of these applications, you want a monitoring application to watch for one or more specific log entries to be written to the event log. For example, you might want to watch for a log entry that indicates that an application encountered a critical error or shut down unexpectedly. These log entries should be reported in real time.
Monitoring an event log for a specific entry requires the following steps:
Create the following method to set up the event handler to handle event log writes:
public void WatchForAppEvent(EventLog log) { log.EnableRaisingEvents = true; // Hook up the System.Diagnostics.EntryWrittenEventHandler. log.EntryWritten += new EntryWrittenEventHandler(OnEntryWritten); }
Create the event handler to examine the log entries and determine whether further action is to be performed. For example:
public static void OnEntryWritten(object source, EntryWrittenEventArgs entryArg) { if (entryArg.Entry.EntryType == EventLogEntryType.Error) { Console.WriteLine(entryArg.Entry.Message); Console.WriteLine(entryArg.Entry.Category); Console.WriteLine(entryArg.Entry.EntryType.ToString()); // Do further actions here as necessary... } }
This recipe revolves around the EntryWrittenEventHandler
delegate, which calls back to a method whenever any new entry is written to the event log. The EntryWrittenEventHandler
delegate accepts two arguments: a source of type object
and an entryArg
of type EntryWrittenEventArgs
. The entryArg
parameter is the more interesting of the two. It contains a property called Entry
that returns an EventLogEntry
object. This EventLogEntry
object contains all the information you need concerning the entry that was written to the event log.
This event log that you are watching is passed as the WatchForAppEvent
method’s log
parameter. This method performs two actions. First, it sets log
’s EnableRaisingEvents
property to true
. If this property were set to false
, no events would be raised for this event log when an entry is written to it. The second action this method performs is to add the OnEntryWritten
callback method to the list of event handlers for this event log.
To prevent this delegate from calling the OnEntryWritten
callback method, you can set the EnableRaisingEvents
property to false
, effectively turning off the delegate.
Note that the Entry
object passed to the entryArg
parameter of the OnEntryWritten
callback method is read-only, so the entry cannot be modified before it is written to the event log.
The “EventLog.EntryWritten Event” topic in the MSDN documentation.
You need to use a performance counter to track application-specific information. The simpler performance counters find, for example, the change in a counter value between successive samplings or just count the number of times an action occurs. Other, more complex counters exist but are not dealt with in this recipe. For example, you could build a custom counter to keep track of the number of database transactions, the number of failed network connections to a server, or even the number of users connecting to your web service per minute.
Create a simple performance counter that finds, for example, the change in a counter value between successive samplings or that just counts the number of times an action occurs. Use the following method (CreateSimpleCounter
) to create a simple custom counter:
public static PerformanceCounter CreateSimpleCounter(string counterName, string counterHelp, PerformanceCounterType counterType, string categoryName, string categoryHelp) { CounterCreationDataCollection counterCollection = new CounterCreationDataCollection(); // Create the custom counter object and add it to the collection of counters CounterCreationData counter = new CounterCreationData(counterName, counterHelp, counterType); counterCollection.Add(counter); // Create category if (PerformanceCounterCategory.Exists(categoryName)) PerformanceCounterCategory.Delete(categoryName); PerformanceCounterCategory appCategory = PerformanceCounterCategory.Create(categoryName, categoryHelp, PerformanceCounterCategoryType.SingleInstance, counterCollection); // Create the counter and initialize it PerformanceCounter appCounter = new PerformanceCounter(categoryName, counterName, false); appCounter.RawValue = 0; return (appCounter); }
The first action this method takes is to create the CounterCreationDataCollection
object and CounterCreationData
object. The CounterCreationData
object is created using the counterName
, counterHelp
, and counterType
parameters passed to the CreateSimpleCounter
method. The CounterCreationData
object is then added to the counterCollection
.
The ASPNET
user account, as well as many other user accounts, by default prevents performance counters from being read for security reasons. You can either increase the permissions allowed for these accounts or use impersonation with an account that has access to enable this functionality. However, this then becomes a deployment requirement of your application.
There is also risk in doing this, as you as the developer are the first line of defense for security matters. If you build it and make choices that loosen security restrictions, you are assuming that responsibility, so please don’t do it indiscriminately or without a full understanding of the repercussions.
If categoryName
—a string containing the name of the category that is passed as a parameter to the method—is not registered on the system, a new category is created from a PerformanceCounterCategory
object. If one is registered, it is deleted and created anew. Finally, the actual performance counter is created from a PerformanceCounter
object. This object is initialized to 0
and returned by the method. PerformanceCounterCategory
takes a PerformanceCounterCategoryType
as a parameter. The possible settings are shown in Table 5-1.
Name | Description |
---|---|
MultiInstance |
There can be multiple instances of the performance counter. |
SingleInstance |
There can be only one instance of the performance counter. |
Unknown |
Instance functionality for this performance counter is unknown. |
The CreateSimpleCounter
method returns a PerformanceCounter
object that will be used by an application. The application can perform several actions on a PerformanceCounter
object. An application can increment or decrement it using one of these three methods:
long value = appCounter.Increment(); long value = appCounter.Decrement(); long value = appCounter.IncrementBy(i); // Additionally, a negative number may be passed to the // IncrementBy method to mimic a DecrementBy method // (which is not included in this class). For example: long value = appCounter.IncrementBy(-i);
The first two methods accept no parameters, while the third accepts a long
containing the number by which to increment the counter. All three methods return a long type indicating the new value of the counter.
In addition to incrementing or decrementing this counter, you can also take samples of the counter at various points in the application. A sample is a snapshot of the counter and all of its values at a particular instance in time. You may take a sample using the following line of code:
CounterSample counterSampleValue = appCounter.NextSample();
The NextSample
method accepts no parameters and returns a CounterSample
structure.
At another point in the application, a counter can be sampled again, and both samples can be passed in to the static Calculate
method on the CounterSample
class. These actions may be performed on a single line of code as follows:
float calculatedSample = CounterSample.Calculate(counterSampleValue, appCounter.NextSample());
The calculated sample calculatedSample
may be stored for future analysis.
The simpler performance counters already available in the .NET Framework are:
CounterDelta32
/CounterDelta64
CounterDelta64
counter can hold larger values than CounterDelta32
.CounterTimer
CounterTimer
value change over the CounterTimer
time change. Tracks the average active time for a resource as a percentage of the total sample time.CounterTimerInverse
CounterTimer
counter. Tracks the average inactive time for a resource as a percentage of the total sample time.CountPerTimeInterval32
/CountPerTimeInterval64
ElapsedTime
NumberOfItems32
/NumberOfItems64
NumberOfItems64
counter can hold larger values than NumberOfItems32
. This counter does not need to be passed to the static Calculate
method of the CounterSample
class; there are no values that must be calculated. Instead, use the RawValue
property of the PerformanceCounter
object (i.e., in this recipe, the appCounter.RawValue
property would be used).RateOfCountsPerSecond32
/RateOfCountsPerSecond64
RateOfCountsPerSecond*
value change over the RateOfCountsPerSecond*
time change, measured in seconds. The RateOfCountsPerSecond64
counter can hold larger values than the RateOfCountsPerSecond32
counter.Timer100Ns
Timer100nsInverse
The “PerformanceCounter Class,” “PerformanceCounterType Enumeration,” “PerformanceCounterCategory Class,” “ASP.NET Impersonation,” and “Monitoring Performance Thresholds” topics in the MSDN documentation.
Add a DebuggerDisplayAttribute
to your class to make the debugger show you something you consider useful about your class. For example, if you had a Citizen
class that held the honorific and name information, you could add a DebuggerDisplayAttribute
like this one:
[DebuggerDisplay("Citizen Full Name = {Honorific}{First}{Middle}{Last}")] public class Citizen { public string Honorific { get; set; } public string First { get; set; } public string Middle { get; set; } public string Last { get; set; } }
Now, when instances of the Citizen
class are instantiated, the debugger will show the information as directed by the DebuggerDisplayAttribute
on the class. To see this, instantiate two Citizen
s, Mrs. Alice G. Jones and Mr. Robert Frederick Jones, like this:
Citizen mrsJones = new Citizen() { Honorific = "Mrs.", First = "Alice", Middle = "G.", Last = "Jones" }; Citizen mrJones = new Citizen() { Honorific = "Mr.", First = "Robert", Middle = "Frederick", Last = "Jones" };
When this code is run under the debugger, the custom display is used, as shown in Figure 5-9.
It is nice to be able to quickly see the pertinent information for classes that you are creating, but the more powerful part of this feature is the ability for your team members to quickly understand what this class instance holds. The this
pointer is accessible from the DebuggerDisplayAttribute
declaration, but any properties accessed via the this
pointer will not evaluate the property attributes before processing. Essentially, if you access a property on the current object instance as part of constructing the display string, if that property has attributes, they will not be processed, and therefore you may not get the value you thought you would. If you have custom ToString()
overrides in place already, the debugger will use these as the DebuggerDisplayAttribute
without your specifying it, provided the correct option is enabled under ToolsOptionsDebugging, as shown in Figure 5-10.
The “Using DebuggerDisplayAttribute” and “DebuggerDisplayAttribute” topics in the MSDN documentation.
Use the CallerMemberName
, CallerFilePath
, and CallerLineNumber
attributes (also known as the Caller Info
attributes) from the System.Runtime.CompilerServices
namespace to determine the calling method.
For example, if you wanted to record the location of the catch
block that caught an exception, you could use a method like RecordCatchBlock
:
public void RecordCatchBlock(Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) { string catchDetails = $"{ex.GetType().Name} caught in member "{memberName}" " + $"in catch block encompassing line {sourceLineNumber} " + $"in file {sourceFilePath} " + $"with message "{ex.Message}""; Console.WriteLine(catchDetails); }
You would then call this method from your catch
blocks like this:
public void TestCallerInfoAttribs() { try { LibraryMethod(); } catch(Exception ex) { RecordCatchBlock(ex); } }
This would allow you to see the type of the exception caught, the class member name it was caught in, and the source file and line number that is encompassed by the catch
block where the exception was caught without having to traverse the call stack:
LibraryException caught in member "TestCallerInfoAttribs" in catch block encompa ssing line 1303 in file C:CSCB6CSharpRecipes 05_DebuggingAndExceptionHandling.cs with message "Object reference not set to an instance of an object."
You could also use this to help determine what method called into a library method, as it can sometimes be difficult to debug which function called the library method:
public void LibraryMethod( [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) { try { // Do some library action // had a problem throw new NullReferenceException(); } catch(Exception ex) { // Wrap the exception and capture the source of where the // library method was called from throw new LibraryException(ex) { CallerMemberName = memberName, CallerFilePath = sourceFilePath, CallerLineNumber = sourceLineNumber }; } }
Using the LibraryException
, you can record at runtime the attributes of the calling method and convey that with the originating exception:
[Serializable] public class LibraryException : Exception { public LibraryException(Exception inner) : base(inner.Message,inner) { } public string CallerMemberName { get; set; } public string CallerFilePath { get; set; } public int CallerLineNumber { get; set; } public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("CallerMemberName", this.CallerMemberName); info.AddValue("CallerFilePath", this.CallerFilePath); info.AddValue("CallerLineNumber", this.CallerLineNumber); } public override string ToString() => "LibraryException originated in " + $"member "{CallerMemberName}" " + $"on line {CallerLineNumber} " + $"in file {CallerFilePath} " + $"with exception details: {Environment.NewLine}" + $"{InnerException.ToString()}"; }
The LibraryException.ToString
method will provide a synopsis of the issue:
LLibraryException originated in member "TestCallerInfoAttribs" on line 1299 in file C:CSCB6CSharpRecipes 5_DebuggingAndExceptionHandling.cs with exception details: System.NullReferenceException: Object reference not set to an instance of an obj etc. at CSharpRecipes.DebuggingAndExceptionHandling.LibraryMethod(String memberNam e, String sourceFilePath, Int32 sourceLineNumber) in D:PRJ32Book_6_0CS60_Cook bookCSCB6CSharpRecipes 5_DebuggingAndExceptionHandling.cs:line 1318
As the CallerInfo
attributes are determined at compile time, there is no cost during runtime to retrieve the information about where the previous method on the stack came from. While not as comprehensive as a full stack trace, it is a cheaper and simpler alternative that could give you method, file, and line information with which you can enhance your exception logging. Any time you can have that sort of information handed to you with a defect/bug report/issue ticket (pick your favorite way to be notified that the code is broken), your life will become much easier.
You may notice that the CallerInfo
attributes require a default value:
public void RecordCatchBlock(Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
Those parameters need a default value because the CallerInfo
attributes were implemented using optional parameters, and optional parameters require a default value. Here’s how you can still call the method hosting the attributes without providing a value for them:
RecordCatchBlock(ex);
See the “CallerMemberNameAttribute,” “CallerFilePathAttribute,” and “CallerLineNumberAttribute” topics in the MSDN documentation.
When you’re handling exceptions from the invocation of a single method, the .NET Framework will handle the return of an exception that occurs between the asynchronous invocation and awaiting of the return. When you’re handling exceptions from the simultaneous invocation of multiple methods asynchronously, a bit more work is required to extract all of the exception detail. Finally, when dealing with the result of an exception, you can call an asynchronous method in the catch
block to handle the work.
To demonstrate this, let’s work with the rather common scenario of a software development team manager named Bill who needs to have some work done.
Bill comes to Steve’s desk and asks him to implement the new feature in this sprint, then walks away, leaving Steve to implement on his own (asynchronously from Bill’s request):
try { // Steve, get that project done! await SteveCreateSomeCodeAsync(); } catch (DefectCreatedException dce) { Console.WriteLine($"Steve introduced a Defect: {dce.Message}"); }
Steve works hard, as all developers do, but even the best of us can have an off day. Steve happens to generate a defect in the feature he was asked to implement in SteveCreateSomeCodeAsync
. Luckily, even though Steve was doing this asynchronously, we can still catch the DefectCreatedException
and handle it normally, as the async
and await
support transports the exception back to the catch
block automatically. (More about how this happens in the Discussion section. Look for ExceptionDispatchInfo
!)
The output we captured from the caught exception lets us know where the issue is so Steve can fix it later:
Steve introduced a Defect: A defect was introduced: (Null Reference on line 42)
Bill knows Steve is busy, so he approaches the other members of the team (Jay, Tom, and Seth) to get a whole new set of features completed in the sprint. It looks like they are going to have to come in on Saturday! Jay, Tom, and Seth get together, divide up the work, and all start coding at the same time. Even though they may finish at different times, Bill still wants to be on the lookout for any defects they created:
// OK Team, make that new thing this weekend! You guys better hurry up with that! Task jayCode = JayCreateSomeCodeAsync(); Task tomCode = TomCreateSomeCodeAsync(); Task sethCode = SethCreateSomeCodeAsync(); Task teamComplete = Task.WhenAll(new Task[] { jayCode, tomCode, sethCode }); try { await teamComplete; } catch { // Get the messages from the exceptions thrown from // the set of actions var defectMessages = teamComplete.Exception?.InnerExceptions.Select(e => e.Message).ToList(); defectMessages?.ForEach(m => Console.WriteLine($"{m}")); }
First, each unit of work (JayCreateSomeCodeAsync
, TomCreateSomeCodeAsync
, SethCreateSomeCodeAsync
) is turned into a Task
. The Task.WhenAll
method is then called to create an encompassing Task
(teamComplete
), which will complete when all of the individual Task
s are completed.
Once all of the tasks have completed, the await
will throw an AggregateException
if any of the Task
s threw an Exception
during execution. This AggregateException
is accessed on the teamComplete.Exception
property, and it holds a list of InnerException
s, which is of type ReadOnlyCollection<Exception>
.
Since the developers introduced this set of exceptions, they are more DefectCreatedException
s!
The resulting logging tells us where the team will need to clean up:
A defect was introduced: (Ambiguous Match on line 2) A defect was introduced: (Quota Exceeded on line 11) A defect was introduced: (Out Of Memory on line 8)
Finally, Bill realizes that he needs a better system to determine if there were defects introduced into the code. Bill adds logging to the code that will write the details of the defect to the EventLog
when a DefectCreatedException
is caught. Since writing to the EventLog
can be a hit to performance, he decides to do this asynchronously.
try { await SteveCreateSomeCodeAsync(); } catch (DefectCreatedException dce) { await WriteEventLogEntryAsync("ManagerApplication", dce.Message, EventLogEntryType.Error); throw; }
Running code asynchronously doesn’t mean you don’t still need to have proper error handling, it just complicates the process a bit. Luckily, the C# and .NET teams at Microsoft have done a lot to make this task as painless as possible.
Awaiting these operations means that they are run in a context, either the current SynchronizationContext
or the TaskScheduler
. This context is captured when the async
method awaits another method, and is restored when work resumes in the async
method.
The context captured depends on where your async
method code is executing:
User interface (WinForms/WPF): UI context
ASP.NET: ASP.NET request context
Other: ThreadPool context
In user story 1, we mentioned that the implementation of async
and await
used a class called System.Runtime.ExceptionServices.ExceptionDispatchInfo
to handle when an exception is thrown on one thread and needs to be caught on another as the result of an asynchronous operation.
ExceptionDispatchInfo
allows the capture of an exception that was thrown in one thread and then allows it to be rethrown—without losing any of the information (exception data and stack trace)—from another thread. This is what happens from an error-handling standpoint when you await
an async
method.
One other item of note is the use of ConfigureAwait
, which allows you to change the behavior of context resumption after the async
method has completed. If you pass false
to ConfigureAwait
, it will not attempt to resume the original context:
await MyAsyncMethod().ConfigureAwait(false);
If you use ConfigureAwait(false)
, then any code after the await
finishes and the async
method resumes cannot rely on the original context, as the thread on which the code continues will not have it. If this async
method were called from an ASP.NET context, for example, the request context will not be available once it resumes.
The code for the hardworking team is presented in Example 5-7.
public async Task TestHandlingAsyncExceptionsAsync() { // Team producing software // Manager sends Steve to create code, exception"DefectCreatedException" thrown // Manager sends Jay, Tom, Seth to write code, all throw DefectCreatedExceptions // Single async method call try { // Steve, get that project done! await SteveCreateSomeCodeAsync(); } catch (DefectCreatedException dce) { Console.WriteLine($"Steve introduced a Defect: {dce.Message}"); } // Multiple async methods (WaitAll) // OK Team, make that new thing this weekend! You guys better hurry up with that! Task jayCode = JayCreateSomeCodeAsync(); Task tomCode = TomCreateSomeCodeAsync(); Task sethCode = SethCreateSomeCodeAsync(); Task teamComplete = Task.WhenAll(new Task[] { jayCode, tomCode, sethCode }); try { await teamComplete; } catch { // Get the messages from the exceptions thrown from // the set of actions var defectMessages = teamComplete.Exception?.InnerExceptions.Select(e => e.Message).ToList(); defectMessages?.ForEach(m => Console.WriteLine($"{m}")); } // awaiting an action in an exception handler // discuss how the original throw location is preserved via // System.Runtime.ExceptionServices.ExceptionDispatchInfo try { try { await SteveCreateSomeCodeAsync(); } catch (DefectCreatedException dce) { Console.WriteLine(dce.ToString()); await WriteEventLogEntry("ManagerApplication", dce.Message, EventLogEntryType.Error); throw; } } catch(DefectCreatedException dce) { Console.WriteLine(dce.ToString()); } } public async Task WriteEventLogEntryAsync(string source, string message, EventLogEntryType type) { await Task.Factory.StartNew(() => EventLog.WriteEntry(source, message, type)); } public async Task SteveCreateSomeCodeAsync() { Random rnd = new Random(); await Task.Delay(rnd.Next(100, 1000)); throw new DefectCreatedException("Null Reference",42); } public async Task JayCreateSomeCodeAsync() { Random rnd = new Random(); await Task.Delay(rnd.Next(100, 1000)); throw new DefectCreatedException("Ambiguous Match",2); } public async Task TomCreateSomeCodeAsync() { Random rnd = new Random(); await Task.Delay(rnd.Next(100, 1000)); throw new DefectCreatedException("Quota Exceeded",11); } public async Task SethCreateSomeCodeAsync() { Random rnd = new Random(); await Task.Delay(rnd.Next(100, 1000)); throw new DefectCreatedException("Out Of Memory", 8); }
The custom DefectCreatedException
is listed in Example 5-8.
[Serializable] public class DefectCreatedException : Exception { #region Constructors // Normal exception ctor's public DefectCreatedException() : base() { } public DefectCreatedException(string message) : base(message) { } public DefectCreatedException(string message, Exception innerException) : base(message, innerException) { } // Exception ctor's that accept the new parameters public DefectCreatedException(string defect, int line) : base(string.Empty) { this.Defect = defect; this.Line = line; } public DefectCreatedException(string defect, int line, Exception innerException) : base(string.Empty, innerException) { this.Defect = defect; this.Line = line; } // Serialization ctor protected DefectCreatedException(SerializationInfo exceptionInfo, StreamingContext exceptionContext) : base(exceptionInfo, exceptionContext) { } #endregion // Constructors #region Properties public string Defect { get; } public int Line { get; } public override string Message => $"A defect was introduced: ({this.Defect ?? "Unknown"} on line {this.Line})"; #endregion // Properties #region Overridden methods // ToString method public override string ToString() => $"{Environment.NewLine}{this.ToFullDisplayString()}"; // Used during serialization to capture information about extra fields [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("Defect", this.Defect); info.AddValue("Line", this.Line); } #endregion // Overridden methods public string ToBaseString() => (base.ToString()); }
The “async,” “await,” “AggregateException,” “ConfigureAwait,” and “System.Runtime.ExceptionServices.ExceptionDispatchInfo” topics in the MSDN documentation.
Use exception filters to catch only exceptions where you want to do something about the condition.
As an example, say you called a database and you wanted to handle timeouts differently. If you were calling from ASP.NET WebApi
, you might even want to return a 503 Service Unavailable message to indicate that the service is busy when you start seeing timeout errors from the database.
The ProtectedCallTheDatabase
method wraps the CallTheDatabase
method in a try
-catch
block and adds an exception filter (using the when
keyword) to check for a DatabaseException
where the allotted Number
property is set to –2
. When the Number
property is set to –2
on a DatabaseException
, it indicates a timeout (much like a current Microsoft database offering) and we will catch the exception and can handle it from there. If Number
is not set to –2
, we will not catch the exception and it will propagate up the call stack to the caller of the ProtectedCallTheDatabase
method:
private void ProtectedCallTheDatabase(string problem) { try { CallTheDatabase(problem); Console.WriteLine("No error on database call"); } catch (DatabaseException dex) when (dex.Number == -2) // watch for timeouts { Console.WriteLine( "DatabaseException catch caught a database exception: " + $"{dex.Message}"); } }
The CallTheDatabase
method simulates calling the database and encountering a problem:
private void CallTheDatabase(string problem) { switch (problem) { case "timeout": throw new DatabaseException( "Timeout expired. The timeout period elapsed prior to " + "completion of the operation or the server is not " + "responding. (Microsoft SQL Server, Error: -2).") { Number = -2, Class = 11 }; case "loginfail": throw new DatabaseException("Login failed for user") { Number = 18456, }; } }
We can call the ProtectedCallTheDatabase
method in the three ways shown in Example 5-9.
Console.WriteLine("Simulating database call timeout"); try { ProtectedCallTheDatabase("timeout"); } catch(Exception ex) { Console.WriteLine($"Exception catch caught a database exception: {ex.Message}"); } Console.WriteLine(""); Console.WriteLine("Simulating database call login failure"); try { ProtectedCallTheDatabase("loginfail"); } catch (Exception ex) { Console.WriteLine($"Exception catch caught a database exception: {ex.Message}"); } Console.WriteLine(""); Console.WriteLine("Simulating successful database call"); try { ProtectedCallTheDatabase("noerror"); } catch (Exception ex) { Console.WriteLine($"Exception catch caught a database exception: {ex.Message}"); } Console.WriteLine("");
We get the output shown in Example 5-10.
Simulating database call timeout DatabaseException catch caught a database exception: Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. (Microsoft SQL Server, Error: -2). Simulating database call login failure Exception catch caught a database exception: Login failed for user Simulating successful database call No error on database call
Note that the timeout was caught in the catch
block in ProtectedCallTheDatabase
, while the login failure was not caught until it returned to the catch
block in the testing code.
Exception filters allow you to conditionally evaluate if a catch
block should catch an exception, which is quite powerful and allows you to handle only exceptions you can do something about at a finer-grained level than was previously possible.
Another advantage of using exception filters is that it does not require the constant catching and rethrowing of exceptions. When this is done improperly it can affect the call stack of the exception and hide errors (see Recipe 5.1 for more details), whereas exception filters let you examine and even perform operations with exceptions (like logging) without interfering with the original flow of the exception. In order to not interfere, the code executed in the exception filter must return false
so that the exception continues to propagate normally. The code introduced to make the true
or false
determination in the exception filter should be kept to a minimum, as you are in a catch
handler and the same rules apply. Don’t do things that could cause other exceptions and mask the original error condition you were trying to trap for in the first place.
The full listing for the DatabaseException
is shown in Example 5-11.
[Serializable] public class DatabaseException : DbException { public DatabaseException(string message) : base(message) { } public byte Class { get; set; } public Guid ClientConnectionId { get; set; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public SqlErrorCollection Errors { get; set; } public int LineNumber { get; set; } public int Number { get; set; } public string Procedure { get; set; } public string Server { get; set; } public override string Source => base.Source; public byte State { get; set; } public override void GetObjectData(SerializationInfo si, StreamingContext context) { base.GetObjectData(si, context); } }
The “Exception Filters” topic in the MSDN documentation.