Exception Handling (1), In ASP.NET MVC

Exception handling is required in any application. It is a very interesing issue where different apps have their own various way(s) to handle that. I plan to write a series of articles to discuss this issue
 
In this article, we will be discussing various ways of handling an exception in ASP.NET MVC.
 

Introduction

 
In ASP.NET MVC we may have three ways to handle exceptions,
  • Try-catch-finally
  • Exception filter
  • Application_Error event
 Furthermore, for filter approch, we have default or customized ones where a customized one may go into local or global, such as
  • Try-catch-finally
  • Exception filter
    • Default (Associated with [HandleError] attribute)
    • Customized (Overriding OnException method)
      • Local
      • Global (Associated with attribute)
  • Application_Error event

Discussion

 
We will make a sample app, and then discuss some details and features in the next section,
  • Step 0: Create an ASP.NET MVC application
  • Step 1-1: Error handling by default Exception Filter
  • Step 1-2: Default Exception Filter, with [HandleError] attribute
  • Step 1-3: Default Exception Filter, Handling the 404 Page Not Foud error
  • Step 2-1: Error handling by customized Exception Filter Globally
  • Step 2-2: Error handling by customized Exception Filter Locally
  • Step 3: Error handling by Try-Catch
  • Step 4: Error handling by Application_Error event
At the end, you have an MVC app that can consume a database directly through entity framework.
 

Step 0 - Create an ASP.NET MVC app

 
We use the current version of Visual Studio 2019 16.8 and .NET Framework 4.7.2 to build the app.
  • Start Visual Studio and select Create a new project.
  • In the Create a new project dialog, select ASP.NET Web Application (.NET Framework) > Next.
  • In the Configure your new project dialog, enter ErrorHandling for Project name > Create.
  • In the Create a new ASP.NET Web Application dialog, select MVC > Creat
Build and run the app, you will see the following image shows the app,
 
 

Step 1-1: Error handling by default Exception Filter

 
Add one empty controller into the app, with name as ErrorHandlingController:
  • Right click Controllers > add > controller.
  • In the Add New Scaffolded Item dialog, select Empty Controller > Add.
  • In the Add Controller dialog, Change ErrorHandlingController for controller name > Add.
Run the app, and route to the page https://localhost:44318/ErrorHandling (where the port will be different in your project), we got the Error below because this controller does not have a view asigned to, and no error handling at all so far,
 
 
Now, we add one line code in the app web.config file,
  1. <system.web>    
  2.   <compilation debug="true" targetFramework="4.7.2"/>    
  3.   <httpRuntime targetFramework="4.7.2"/>    
  4.   <customErrors mode="On">  </customErrors>    
  5. </system.web>    
The magic happens that after re-run the app, the page, https://localhost:44318/ErrorHandling, is redirected to a default Error handing page,
 
 
The error has been handled by the MVC system, actually, the default, MVC built in exception filter.
 
What is MVC filter? In ASP.NET MVC, a user request is routed to the appropriate controller and action method. Before or after an action method executes, there may be circumstances where some logic might need to be executed. This is done by ASP.NET MVC filters (more discussion, see 《ASP.NET MVC- Filters》).
 
MVC provides different types of built in filters,
  • Authorization filters
  • Resource filters
  • Action filters
  • Exception filters
  • Result filters
 
The HandleErrorAttribute class is a built-in exception filter class that renders the Error.cshtml by default when an unhandled exception occurs. 
 
 

Step 1-2: Default Exception Filter, with [HandleError] attribute

 
Now, we release more features about the default MVC exception filter. In the ErrorHandling Controller, we add two more actions: index1 and index2 associated with [HandleError] attribute,
  1. using System;    
  2. using System.Web.Mvc;    
  3.     
  4. namespace ErrorHandling.Controllers    
  5. {    
  6.     public class ErrorHandlingController : Controller    
  7.     {    
  8.         // GET: ErrorHandling    
  9.         public ActionResult Index()    
  10.         {    
  11.             return View();    
  12.         }    
  13.     
  14.         [HandleError]    
  15.         [HandleError(ExceptionType = typeof(DivideByZeroException), View = "Error1")]    
  16.         [HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "Error2")]    
  17.         public ActionResult Index1()    
  18.         {    
  19.             int a = 1;    
  20.             int b = 0;    
  21.             int c = 0;    
  22.             c = a / b; //it would cause exception.                 
  23.             return View();    
  24.         }    
  25.     
  26.     
  27.         [HandleError(ExceptionType = typeof(DivideByZeroException), View = "Error1")]    
  28.         [HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "Error2")]    
  29.         [HandleError] // this will work, go to the default: view = "Error";    
  30.         public ActionResult Index2()    
  31.         {    
  32.             int a = 1;    
  33.             int b = 0;    
  34.             int c = 0;    
  35.             c = a / b; //it would cause exception.                 
  36.             return View();    
  37.         }    
  38.     }    
  39. }   
And in the project view folder, we add another view, named Error1.cshtml,
 
 
where the content is exactly the same as Error.cshtml, except changing Error. to Error 1 ( Note: For the later test purpose, we will add three more views in the View/Shared folder: Error2.cshtml, Error3.cshtml, and Error4.cshtm with the same as Error1.cshtml.) 
  1. <!DOCTYPE html>    
  2. <html>    
  3. <head>    
  4.     <meta name="viewport" content="width=device-width" />    
  5.     <title>Error</title>    
  6. </head>    
  7. <body>    
  8.     <hgroup>    
  9.         <h1>Error 1.</h1>    
  10.         <h2>An error occurred while processing your request.</h2>    
  11.     </hgroup>    
  12. </body>    
  13. </html>   
Now, run the app, the results will be,
 
 
This is the same as previous: https://localhost:44318/errorhandling/index ==> Default Error page;
 
 
https://localhost:44318/errorhandling/index1 ==> Error 1 page, due to attribute: [HandleError(ExceptionType = typeof(DivideByZeroException), View = "Error1")]
 
 
https://localhost:44318/errorhandling/index1 ==> Default Error page, due to attribute: [HandleError] --- notice the order of the arttribute!
 

Step 1-3: Default Exception Filter, Handling the 404 Page Not Foud error

 
Run the app, and try this: https://localhost:44318/errorhandling/index3
 
 
The page is not foud, but this error has not been handled by the default exception filter. We have to handle it by a customized Error Page like this,
  1. <system.web>    
  2.   <compilation debug="true" targetFramework="4.7.2"/>    
  3.   <httpRuntime targetFramework="4.7.2"/>    
  4.   <customErrors mode="On">    
  5.     <error statusCode="404" redirect="~/ErrorPage"/>    
  6.   </customErrors>    
  7. </system.web>    
Now, we add a ErrorPage into the app,
  • Add a new empty controller as same as above with name ErrorPageController.
  • Open the controller class (see below), right click the Index > Add View >
  • In the Add New Scaffolded Item dialog, select MVC 5 View > Add.
  • In the Add New dialog, keep index for view name > Add.
    1. using System.Web.Mvc;    
    2.     
    3. namespace ErrorHandling.Controllers    
    4. {    
    5.     public class ErrorPageController : Controller    
    6.     {    
    7.         // GET: ErrorPage    
    8.         public ActionResult Index()    
    9.         {    
    10.             return View();    
    11.         }    
    12.     }    
    13. }    
Open the created View/ErrorPage/index.cshtml, and change the content to,
  1. @{    
  2.     ViewBag.Title = "Index";    
  3. }    
  4. <h2>Customized Error Page</h2>    
Now, run the app, we have: https://localhost:44318/errorhandling/index3 ==> Customized Error Page
 
 

Step 2-1: Error handling by Customized Exception Filter Globally

 
This is implemented in Global.asax by overriding OnException method from a class derived from HandleErrorAttribute class, such as
  1. public class MyExceptionHandler : HandleErrorAttribute    
  2. {    
  3.     public override void OnException(ExceptionContext filterContext)    
  4.     {    
  5.         Exception e = filterContext.Exception;    
  6.         filterContext.ExceptionHandled = true;    
  7.         filterContext.Result = new ViewResult()    
  8.         {    
  9.             ViewName = "Error4"    
  10.         };    
  11.     }    
  12. }   
We add one more action in the ErrorHandling Controller,
  1. [MyExceptionHandler]    
  2. [HandleError(ExceptionType = typeof(DivideByZeroException), View = "Error1")]    
  3. [HandleError(ExceptionType = typeof(ArgumentOutOfRangeException), View = "Error2")]    
  4. [HandleError] // this will work, go to the default: view = "Error";    
  5. public ActionResult Index4()    
  6. {    
  7.     int a = 1;    
  8.     int b = 0;    
  9.     int c = 0;    
  10.     c = a / b; //it would cause exception.                 
  11.     return View();    
  12. }   
Run the app, and try this: https://localhost:44318/errorhandling/index4, we get,
 
 

Step 2-2: Error handling by Customized Exception Filter Locally

 
This is implemented in the local page: ErrorHandling.cs by overriding OnException method from the controller class, such as 
  1. protected override void OnException(ExceptionContext filterContext)    
  2. {    
  3.     string action = filterContext.RouteData.Values["action"].ToString();    
  4.     Exception e = filterContext.Exception;    
  5.     filterContext.ExceptionHandled = true;    
  6.     var model = new HandleErrorInfo(filterContext.Exception, "Controller""Action");    
  7.     filterContext.Result = new ViewResult()    
  8.     {    
  9.         ViewName = "Error3",    
  10.         ViewData = new ViewDataDictionary(model)    
  11.     };    
  12. }   
Run the app, and try this: https://localhost:44318/errorhandling/index4, the local filter will be executed first, then we get Error3 page:
 
 

Step 3 - Error handling by Try-Catch

 
We add another action in ErrorHandling.cs,
  1. public ActionResult Index5()    
  2. {    
  3.     int a = 1;    
  4.     int b = 0;    
  5.     int c = 0;    
  6.     try    
  7.     {    
  8.         c = a / b; //it would cause exception.      
  9.     }    
  10.     catch (Exception ex)    
  11.     {    
  12.         return View("Error2");    
  13.     }    
  14.     finally    
  15.     {    
  16.     }    
  17.     return View();    
  18. }    
Run the app, and try this: https://localhost:44318/errorhandling/index5, the try-catch block will be executed first, then we get Error2 page,
 
 
In a Catch block, you can log the error either in a file or in a database.
 

Step 4 - Error handling by Application_Error event 

 
This is implemented in Global.asax as traditional asp.net web forms
  1. public class MvcApplication : System.Web.HttpApplication    
  2. {    
  3.     protected void Application_Start()    
  4.     {    
  5.         AreaRegistration.RegisterAllAreas();    
  6.         FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);    
  7.         RouteConfig.RegisterRoutes(RouteTable.Routes);    
  8.         BundleConfig.RegisterBundles(BundleTable.Bundles);    
  9.     }    
  10.     
  11.     protected void Application_Error(object sender, EventArgs e)    
  12.     {    
  13.         Exception exception = Server.GetLastError();    
  14.         Server.ClearError();    
  15.         Response.Redirect("~/ErrorPage");    
  16.     }    
  17. }   
This is the Outermost error handling, if we did not have any error handling this event will catch the unhandled errors, but all other error hadling will be executed before this event.
 

Summary

 
The ASP.NET MVC Exception handling can be categaried by type as,
  • Try-catch-finally
  • Exception filter
    • Default (Associated with [HandleError] attribute)
    • Customized (Overriding OnException method)
      • Local
      • Global (Associated with attribute)
  • Application_Error event
Or categaried by Local/Global,
  • Local
    • Try-catch-finally
    • Customized local filter (Overriding OnException method)
  • Global
    • Default Filter (Associated with [HandleError] attribute)
    • Customized Global Filter (Overriding OnException method)
    • Application_Error event
 The order to exection is from intermost to outermost as,
  1. Try-catch-finally ==> 
  2. Customized local filter ==>
  3. Customized Global Filter ==>
  4. Default Filter ==>
  5. Application_Error event 
References