Custom Error Handling in ASP.NET MVC

This post covers custom error and exception handling in ASP.NET MVC with more generic approach to satisfy the need of today’s development, today’s development doesn’t mean that we are going to handle the error in a different way than currently supported by MVC but handling error slightly different to allow more responsive and user friendly UI.

If you are looking for ASP.NET MVC in-built exception handling then this post may not cover it in its entirety. You will get several other posts talking about MVC exception handling in much better way. I prefer MVC’s HandleErrorAttribute with some customization to handle all Http 500 errors, anything beyond 500 has to be handled by Application’s Error event. I prefer to outsource Application level error handling to a common Http Module; it can be plugged in to any application by some generic implementation and configuration.

MVC’s default HandleErrorAttribute is really good and serves all purpose for a good basic exception handling mechanism but it requires customization, if you need to perform logging or action action.

Scenario

The goal of this post is to handle a complex scenario where a single view might have multiple child views rendered like header, footer, carousel, multiple widgets, and scenario like content managed websites. Let’s talk about from the user perspective, who is trying to visit your site, for him the most important stuff is his business with your site, he may not be interested in weather an widget appearing at the corner of your site or any other less concerning area. He comes to your site and suddenly the weather widget fails, it could be due to your weather service provider being down or some network issue or any other reason but as the action method responsible to display the weather information fails, your standard error handling mechanism will take him to standard error page. You can argue that I will add a try catch block to handle the error and display a custom error message to the user but if you have got 5 widgets then you need to do the same for every action method, repeating the same code every time with different error message not to mention it will become more difficult in content managed environment.

A better way to handle it

To find a better way, we need to identify what is important for user and what is less important. Something important will always take user to Error Page and less important will make the page to render with custom message or possible remove the view or it would be better if it replaces the view with some other view.

Building Blocks

Let’s talk in terms of MVC, we will still use the IExceptionFilter or HandleError with some customization to achieve it, but first we will identify what we want to do in different scenarios.

1) Redirect – Most important and default behavior required for system.

2) Replace – Important but can be replaced by a different view.

3) ShowError – Less important, pages can render without any issue and failed widgets will display custom messages

4) Hide – Least important, erred area or view can be removed from the page.

Now an enum is required to handle these scenarios.

    ///
    /// ActionType
    ///
    public enum ActionType
    {
       ///
       /// Redirect will abort the execution of current page will redirect to an error page
       ///
       Redirect = 0,

       ///
       /// Replace the view with a different view
       ///
       Replace = 1,

       ///
       /// ShowError will show the custom error message
       ///
       ShowError = 2,

       ///
       /// Hide will hide the custom error message and display empty result
       ///
       Hide = 3
}

Error Model to pass the error information and custom message to the view

   
    ///
    /// ErrorModel
    /// A custom error model class to pass the error info
    ///
    public class ErrorModel : HandleErrorInfo 
    { 
       ///
       /// Displayable error message 
       ///
       public string ErrorMessage { get; set; } 

       public ErrorModel(string errorMessage, Exception exception, string controllerName, string actionName) :base(exception, controllerName, actionName) 
      { 
           this.ErrorMessage = errorMessage; 
      } 
    }

Now we have the scenarios covered but we also need to modify the HandleError attribute to handle these scenarios. For brevity, I will implement the interface IExceptionFilter than customizing the HandleError attribute.

     ///
     /// HandleExceptionAttribute
     /// Represents an attribute that is used to handle an exception that is thrown by an action method.
     ///
     [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]   
     public class HandleExceptionAttribute : FilterAttribute, IExceptionFilter 
     {

Some default behavior, it is mostly copied from HandleError attribute –

 
        private Type exceptionType = typeof(Exception);

        private readonly string defaultView = "Error";

        private string view = string.Empty;

        ///
        /// ExceptionType
        ///
        public Type ExceptionType
        { 
            get { return this.exceptionType; } 
            set 
            { 
                   if (value == null) 
                   { 
                       throw new ArgumentNullException("value"); 
                   } 
                   if (!typeof(Exception).IsAssignableFrom(value))
                   { 
                       throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The type {0} does not inherits from the exception", new object[] { value.FullName })); 
                   } 
                   this.exceptionType = value; 
            } 
        } 
        ///
        /// ErrorId 
        /// Error Id to be initialize with Attribute and will be used to call the content service to fetch the appropriate error message. 
        ///
        public string ErrorId { get; set; } 

        ///
        /// Action 
        ///
        public ActionType Action { get; set; } 

        ///
        /// View 
        ///
        public string View 
        { 
           get 
           { 
                if (string.IsNullOrEmpty(this.view)) 
                { 
                     return defaultView; 
                } 
                return this.view; 
           } 
           set 
           {  
                this.view = value; 
           } 
        }

Add the interfaces to handle the logging and Error Message retrieval from content store (config or db), an implementation of IContentService is required to get the message from content store.

     

        #region Properties to handle logging and content retrieval 

        ///
        /// Logger
        /// To log the error messages or any other diagnostic information
        ///
        public ILogger Logger { get; set; } 

        ///
        /// Content Service 
        /// Content Service to retrieve the content from Config or DB 
        ///
        public IContentService ContentService { get; set; } 

        #endregion

Initialization

       

       #region Constructor

        ///
        /// Initializes a new instance of the HandleExceptionAttribute class.
        ///
        public HandleExceptionAttribute() 
             : this(DependencyResolver.Current.GetService(), DependencyResolver.Current.GetService()) 
        { } 
         
        ///
        /// Initializes a new instance of the HandleExceptionAttribute class. 
        ///
        public HandleExceptionAttribute(ILogger logger, IContentService contentService) 
        { 
           this.Logger = logger; 
           this.ContentService = contentService;
        } 
        
        #endregion

Exception Handler

Most of the stuff in OnException method is similar to HandleError except the logic to handle the scenario. This logic is built on the assumption that your individual views are partial view and ShowError and Replace will just swap the view. There will be some customization required to view to display it in the right format as all the views may not be of same size.

Keeping the highest priority in mind, this attribute default the behavior to redirect and in case of nothing specified it will redirect to default error page mentioned in the customError section inside web.config file.


       ///
       /// Called when an exception occurs.
       ///
       public virtual void OnException(ExceptionContext filterContext) 
       { 
            // Bail if we can't do anything; propogate the error for further processing. 
            if (filterContext == null) 
                 return; 
            if (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled) 
            { 
                Exception innerException = filterContext.Exception; 
                if ((new HttpException(null, innerException).GetHttpCode() == 500) && this.ExceptionType.IsInstanceOfType(innerException)) 
                { 
                      string controllerName = (string) filterContext.RouteData.Values["controller"]; 
                      string actionName = (string) filterContext.RouteData.Values["action"]; 
                      // Log exception Logger.Log(innerException.Message); 
                      switch (this.Action) 
                      { 
                             case ActionType.ShowError: 
                                   var errorModel = new ErrorModel(ContentService.GetErrorMessage(this.ErrorId), filterContext.Exception, controllerName, actionName); 
                                   filterContext.Result = new ViewResult { ViewName = this.View, ViewData = new ViewDataDictionary(errorModel), TempData = filterContext.Controller.TempData }; 
                                   break; 
                             case ActionType.Hide: 
                                   filterContext.Result = new EmptyResult(); 
                                   break; 
                             case ActionType.Replace: 
                                   // a logic similar to ShowError required to replace the view 
                                   break; 
                             default: 
                                   var errorSection = ConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection; 
                                   filterContext.Result = new RedirectResult(errorSection.DefaultRedirect);          
                                   break; 
                      } 
                      filterContext.ExceptionHandled = true; 
                      filterContext.HttpContext.Response.Clear(); 
                      filterContext.HttpContext.Response.StatusCode = 500;
                      filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; 
                 }
            } 
       }

Example

Below example will take the default behavior of handle exception attribute and redirect to error page, which can have default or standard error message.


        [HandleException()]
        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";

            return View();
        }

Below example will take the action type as show error and will call the content service to get the standard error message based on the error id specified and render the custom view with the error message.


         [HandleException(Action = ActionType.ShowError, ErrorId = "ErrId1", View = "CustomError", ExceptionType = typeof(ArgumentNullException))]
        public ActionResult About()
        {
            if (User.Identity == null)
                throw new ArgumentNullException("Null Identity");
            return View();
        }
Advertisements

About cprakash

A developer, passionate about .Net, Architecture and Security.
This entry was posted in MVC and tagged , . Bookmark the permalink.

3 Responses to Custom Error Handling in ASP.NET MVC

  1. Pingback: Development Framework for Sitecore MVC | cprakash

  2. Pingback: Exception Handling in Sitecore MVC | cprakash

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s