While working with enterprise applications, I have seen a lot of problems handling the exceptions properly and logging them respectively. It is always a costly task in .NET if you haven't handled this properly in your application.
See the below problems while writing the exceptions,
There is a chance child functions are eating exceptions, and parent function is not aware of the exception.
While working in layered architecture you have to call multiple nested functions. CLR allows you to throw an exception inside the catch block implicitly so that when you use throw directly inside the catch block, it rethrows the same exception object to the caller preserving the caller stack intact. Now, what does it mean exactly? See the example below.
- public void ParentFunction()
- {
- try
- {
- this.ChildFunction();
- Console.WriteLine("After Exception");
- }
- catch (Exception ex)
- {
- }
- }
-
- public void ChildFunction()
- {
- try
- {
- this.GrandChildFunction();
- Console.WriteLine("Child Function");
- }
- catch (Exception ex)
- {
- throw ex;
- }
- }
-
- public void GrandChildFunction()
- {
- try
- {
- int x = 10, y = 0;
- Console.WriteLine(x / y);
- Console.WriteLine("Grand Child Function");
- }
- catch (Exception ex)
- {
- throw;
- }
- }
In the above code, parent function is calling child function and child function is calling grandchild function internally. The exception is actually happening in the grandchild level, same as throwing to its parent, which is child function. But child function, wrapped with the exception object, is throwing to its parent. In this scenario, the actual stack trace is lost.
Finding the hidden exceptions
It is very common for code like this below to be used within applications. The below code can throw thousands of exceptions a minute and nobody would ever know it. Exceptions will occur if the reader is null, columnName is null, column name does not exist in the results, the value for the column was null, or if the value was not a proper DateTime. But unfortunately, we never get the exact exception. If an exception occurs we will always get a null value.
- public DateTime? GetDateTime(SqlDataReader reader, string columnName)
- {
- DateTime? value = null;
- try
- {
- value = DateTime.Parse(reader[columnName].ToString());
- }
- catch
- {
- }
- return value;
- }
Extra overhead to manage the bad code
Exception handling in each separate method is always a costly task because it requires time to review the code, refactor the code and make sure all exceptions are handled perfectly. Also, we need to make sure the code is unit tested properly. Also, it is very difficult to manage to log the exceptions in the different environment. Also if multiple developers are working on a solution they will write their own exception to handle the scenarios.
To overcome all the problems, it is better to handle the predefined exceptions and the custom exceptions in a central location as a middleware. Please see the below steps to handle it in ASP.NET Core.
Step 1
Create a new class called CustomExceptionHandler inside your solution by inheriting IExceptionFilter class. If you want you can create a utility folder and keep it.
-
-
-
- public class CustomExceptionHandler : IExceptionFilter
- {
- public void OnException(ExceptionContext context)
- {
- throw new NotImplementedException();
- }
- }
Step 2
Create another class called CustomException inside the same namespace as below. Here, I have overloaded the constructor with different parameters to call the base class parameterized constructor.
-
-
-
- public class CustomException : Exception
- {
- public CustomException()
- {
- }
-
- public CustomException(string message) : base(message)
- {
- }
-
- public CustomException(string message, string responseModel) : base(message)
- {
- }
-
- public CustomException(string message, Exception innerException) : base(message, innerException)
- {
- }
- }
Step 3
Create an enum called Exceptions for sample exceptions.
-
-
-
- public enum Exceptions
- {
- NullReferenceException = 1,
- FileNotFoundException = 2,
- OverflowException = 3,
- OutOfMemoryException = 4,
- InvalidCastException = 5,
- ObjectDisposedException = 6,
- UnauthorizedAccessException = 7,
- NotImplementedException = 8,
- NotSupportedException = 9,
- InvalidOperationException = 10,
- TimeoutException = 11,
- ArgumentException = 12,
- FormatException = 13,
- StackOverflowException = 14,
- SqlException = 15,
- IndexOutOfRangeException = 16,
- IOException = 17
- }
Step 4
Create a private method to get the exact HTTP status code based on the exception inside the CustomExceptionHandler class.
-
-
-
-
-
- private HttpStatusCode getErrorCode(Type exceptionType)
- {
- Exceptions tryParseResult;
- if (Enum.TryParse<Exceptions>(exceptionType.Name, out tryParseResult))
- {
- switch (tryParseResult)
- {
- case Exceptions.NullReferenceException:
- return HttpStatusCode.LengthRequired;
-
- case Exceptions.FileNotFoundException:
- return HttpStatusCode.NotFound;
-
- case Exceptions.OverflowException:
- return HttpStatusCode.RequestedRangeNotSatisfiable;
-
- case Exceptions.OutOfMemoryException:
- return HttpStatusCode.ExpectationFailed;
-
- case Exceptions.InvalidCastException:
- return HttpStatusCode.PreconditionFailed;
-
- case Exceptions.ObjectDisposedException:
- return HttpStatusCode.Gone;
-
- case Exceptions.UnauthorizedAccessException:
- return HttpStatusCode.Unauthorized;
-
- case Exceptions.NotImplementedException:
- return HttpStatusCode.NotImplemented;
-
- case Exceptions.NotSupportedException:
- return HttpStatusCode.NotAcceptable;
-
- case Exceptions.InvalidOperationException:
- return HttpStatusCode.MethodNotAllowed;
-
- case Exceptions.TimeoutException:
- return HttpStatusCode.RequestTimeout;
-
- case Exceptions.ArgumentException:
- return HttpStatusCode.BadRequest;
-
- case Exceptions.StackOverflowException:
- return HttpStatusCode.RequestedRangeNotSatisfiable;
-
- case Exceptions.FormatException:
- return HttpStatusCode.UnsupportedMediaType;
-
- case Exceptions.IOException:
- return HttpStatusCode.NotFound;
-
- case Exceptions.IndexOutOfRangeException:
- return HttpStatusCode.ExpectationFailed;
-
- default:
- return HttpStatusCode.InternalServerError;
- }
- }
- else
- {
- return HttpStatusCode.InternalServerError;
- }
- }
In the above method, I have mapped the exceptions to relevant status codes. This is just a sample. You can use your own mapping based on your requirement.
Step 5
Implement the OnException method of the parent interface like below.
-
-
-
-
- public void OnException(ExceptionContext context)
- {
- HttpStatusCode statusCode = (context.Exception as WebException != null &&
- ((HttpWebResponse) (context.Exception as WebException).Response) != null) ?
- ((HttpWebResponse) (context.Exception as WebException).Response).StatusCode
- : getErrorCode(context.Exception.GetType());
- string errorMessage = context.Exception.Message;
- string customErrorMessage = Constant.ERRORMSG;
- string stackTrace = context.Exception.StackTrace;
- HttpResponse response = context.HttpContext.Response;
- response.StatusCode = (int) statusCode;
- response.ContentType = "application/json";
- var result = JsonConvert.SerializeObject(
- new
- {
- message = customErrorMessage,
- isError = true,
- errorMessage = errorMessage,
- errorCode = statusCode,
- model = string.Empty
- });
- #region Logging
-
-
-
-
-
-
-
-
-
-
-
-
-
- #endregion Logging
- response.ContentLength = result.Length;
- response.WriteAsync(result);
- }
In the above method I have commented a region called logging which is used for logging in event viewer in my application. You can use your own logging mechanism,
- HttpStatusCode is used to get the exact status code for all web exceptions and customized exceptions.
- ErrorMessage is used for storing the exact error message.
- CustomErrorMessage is used to return the customized error message, here I have used as constant.
- StackTrace is used to log the stack trace of the exception, which will be helpful for troubleshooting the issue for the developers.
- The response is used for returning the response to the client.
- By using JsonConvert class you can serialize the response model into JSON which you need to write to respond. You can wrap up your own response format based on your need.
Step 6
In the startup.cs file please register the exception filter in ConfigureServices method like below.
-
-
-
-
- public void ConfigureServices(IServiceCollection services)
- {
-
- AppStart.RegisterCORS(ref services);
-
- services.AddMvc(
- config =>
- {
- config.Filters.Add(typeof(CustomExceptionHandler));
- config.Filters.Add(typeof(ValidationFilter));
- }
- ).AddFluentValidation();
-
- AppStart.RegisterDIClasses(ref services);
- AppStart.RegisterSwagger(ref services);
- AppStart.ConfigureModelValidation(ref services);
- }
This is a one time setup in your project, no need to write the exception in all methods. Now, you will be free from writing the exceptions. Please mention in the comment section if you have any concerns.