ASP. NET MVC Exception Handling Module Detailed Explanation

  • 2021-07-13 05:02:45
  • OfStack

1. Preface

Exception handling is an essential part of every system, which can make our program prompt and record error information in a friendly way when errors occur, and more importantly, it does not destroy normal data and affect system operation. Exception handling should be a crosscutting point. The so-called crosscutting point means that all parts will use it. No matter which layer in the hierarchy or which specific business logic module, they pay attention to one kind. Therefore, we will deal with crosscutting concerns in one place. Both MVC and WebForm provide such an implementation that we can focus on handling exceptions.

In MVC, in FilterConfig, we have registered an HandleErrorAttribute by default, which is a filter that inherits the FilterAttribute class and implements the IExceptionFilter interface. When it comes to exception handling, you will immediately think of 500 error pages, logging, etc. HandleErrorAttribute can easily customize error pages, and the default is Error pages; To record the log, we only need to inherit it and replace it and register it with GlobalFilterCollection. Many people know how to use HandleErrorAttribute, so we won't introduce it here.

ok, get to the topic! When dealing with exceptions in MVC, I believe many people inherit HandleErrorAttribute at first, and then rewrite OnException method and add their own logic, such as writing exception information to log files. Of course, there is nothing wrong with this, but good design should be scene-driven, dynamic and configurable. For example, in scenario 1, we want ExceptionA to display the error page A, while in scenario 2, we want it to display the error page B, where the scenario may be across projects or in different modules of the same system. In addition, exceptions may also be graded. For example, when ExceptionA occurs, we only need a simple recovery state, and the program can continue to run. When ExceptionB occurs, we want to record it to a file or system log, while when ExceptionC occurs, it is a serious error, and we want the program to be notified by email or SMS. Simply put, different scenarios have different requirements, and our program needs to face changes better. Of course, inheriting HandleErrorAttribute can also achieve the above, but I don't intend to extend it here, but rewrite a module and use it together with the original HandleErrorAttribute.

2. Design and Implementation

2.1 Defining Configuration Information

From the above we can already see what we want to do. We want to configure its handler, error page, and so on for different exceptions. The following 1 configuration:


<!-- Custom Exception Configuration -->
<settingException>
 <exceptions>
  <!--add Priority is higher than group-->
  <add exception="Exceptions.PasswordErrorException"
     view ="PasswordErrorView"
     handler="ExceptionHandlers.PasswordErrorExceptionHandler"/>
  <groups>
   <!--group You can configure 1 Abnormal species view And handler-->
   <group view="EmptyErrorView" handler="ExceptionHandlers.EmptyExceptionHandler">
    <add exception="Exceptions.UserNameEmptyException"/>
    <add exception="Exceptions.EmailEmptyException"/>
   </group>    
  </groups>
 </exceptions>
</settingException>

Among them, add node is used to add specific exceptions, and its exception attribute is necessary, while view represents error page and handler represents specific handler. If there is no view or handler, the exception will be handed over to the default HandleErrorAttribute for handling. The group node is used for grouping, for example, the above UserNameEmptyException and EmailEmptyException correspond to the same handler and view.

The program will reflect and read this configuration information and create the corresponding object. We put this configuration file in Web. config to ensure that it can be changed and taken effect at any time.

2.2 Exception information wrapper object

Here we define an entity object corresponding to the node above. As follows:


public class ExceptionConfig
{
  /// <summary>
  ///  View 
  /// </summary>
  public string View{get;set;}
 
  /// <summary>
  ///  Exception object 
  /// </summary>
  public Exception Exception{get;set;}
 
  /// <summary>
  ///  Exception handler 
  /// </summary>
  public IExceptionHandler Handler{get;set;}
}

2.3 Defining the Handler interface

As we said above, different exceptions may need different handling methods. Here we design an interface as follows:


public interface IExceptionHandler
{
  /// <summary>
  ///  Is the exception handled complete 
  /// </summary>
  bool HasHandled{get;set;}
 
  /// <summary>
  ///  Handle exceptions 
  /// </summary>
  /// <param name="ex"></param>
  void Handle(Exception ex);
}

All kinds of exception handlers only need to implement this interface.

2.3 Implementing IExceptionFilter

This is a must. As follows, the IExceptionFilter interface is implemented, and SettingExceptionProvider gets wrapper objects from configuration information (cache) based on the exception object type.


public class SettingHandleErrorFilter : IExceptionFilter
{
  public void OnException(ExceptionContext filterContext)
  {
    if(filterContext == null)
    {
      throw new ArgumentNullException("filterContext");
    }
    ExceptionConfig config = SettingExceptionProvider.Container[filterContext.Exception.GetType()];
    if(config == null)
    {
      return;
    }
    if(config.Handler != null)
    {
      // Execute Handle Method         
      config.Handler.Handle(filterContext.Exception);
      if (config.Handler.HasHandled)
      {
        // Exception has been handled, no subsequent action is required 
        filterContext.ExceptionHandled = true;
        return;
      }
    }      
    // Otherwise, if there is a custom page, the 
    if(!string.IsNullOrEmpty(config.View))
    {
      // It can also be extended to implement IView View of 
      ViewResult view = new ViewResult();
      view.ViewName = config.View;
      filterContext.Result = view;
      filterContext.ExceptionHandled = true;
      return;
    }
    // Otherwise the exception continues to be passed 
  }
}

2.4 Read the configuration file and create the exception information wrapper object

There is a lot of code in this part. In fact, you only need to know that it is reading the custom configuration node of web. config. SettingExceptionProvider is used to provide container objects.


public class SettingExceptionProvider
{
  public static Dictionary<Type, ExceptionConfig> Container =
    new Dictionary<Type, ExceptionConfig>();
 
  static SettingExceptionProvider()
  {
    InitContainer();
  }
 
  // Read the configuration information and initialize the container 
  private static void InitContainer()
  {
    var section = WebConfigurationManager.GetSection("settingException") as SettingExceptionSection;
    if(section == null)
    {
      return;
    }
    InitFromGroups(section.Exceptions.Groups);
    InitFromAddCollection(section.Exceptions.AddCollection);
  }
 
  private static void InitFromGroups(GroupCollection groups)
  {           
    foreach (var group in groups.Cast<GroupElement>())
    { 
      ExceptionConfig config = new ExceptionConfig();
      config.View = group.View;
      config.Handler = CreateHandler(group.Handler);
      foreach(var item in group.AddCollection.Cast<AddElement>())
      {
        Exception ex = CreateException(item.Exception);
        config.Exception = ex;
        Container[ex.GetType()] = config;
      }
    }
  }
 
  private static void InitFromAddCollection(AddCollection collection)
  {
    foreach(var item in collection.Cast<AddElement>())
    {
      ExceptionConfig config = new ExceptionConfig();
      config.View = item.View;
      config.Handler = CreateHandler(item.Handler);
      config.Exception = CreateException(item.Exception);
      Container[config.Exception.GetType()] = config;
    }
  }
 
  // Created from a fully qualified name IExceptionHandler Object 
  private static IExceptionHandler CreateHandler(string fullName)      
  {
    if(string.IsNullOrEmpty(fullName))
    {
      return null;
    }
    Type type = Type.GetType(fullName);
    return Activator.CreateInstance(type) as IExceptionHandler;
  }
 
  // Created from a fully qualified name Exception Object 
  private static Exception CreateException(string fullName)
  {
    if(string.IsNullOrEmpty(fullName))
    {
      return null;
    }
    Type type = Type.GetType(fullName);
    return Activator.CreateInstance(type) as Exception;
  }
}

The following is the information for each configuration node:

settingExceptions Node:


/// <summary>
/// settingExceptions Node 
/// </summary>
public class SettingExceptionSection : ConfigurationSection
{
  [ConfigurationProperty("exceptions",IsRequired=true)]
  public ExceptionsElement Exceptions
  {
    get
    {
      return (ExceptionsElement)base["exceptions"];
    }
  }
}

exceptions Node:


/// <summary>
/// exceptions Node 
/// </summary>
public class ExceptionsElement : ConfigurationElement
{
  private static readonly ConfigurationProperty _addProperty =
    new ConfigurationProperty("", typeof(AddCollection), null, ConfigurationPropertyOptions.IsDefaultCollection);
 
  [ConfigurationProperty("", IsDefaultCollection = true)]
  public AddCollection AddCollection
  {
    get
    {
      return (AddCollection)base[_addProperty];
    }
  }
 
  [ConfigurationProperty("groups")]
  public GroupCollection Groups
  {
    get
    {
      return (GroupCollection)base["groups"];
    }
  }
}

Group Node Set:


/// <summary>
/// group Node set 
/// </summary>
[ConfigurationCollection(typeof(GroupElement),AddItemName="group")]
public class GroupCollection : ConfigurationElementCollection
{   
  /*override*/
 
  protected override ConfigurationElement CreateNewElement()
  {
    return new GroupElement();
  }
 
  protected override object GetElementKey(ConfigurationElement element)
  {
    return element;
  }
}

group Node:


/// <summary>
/// group Node 
/// </summary>
public class GroupElement : ConfigurationElement
{
  private static readonly ConfigurationProperty _addProperty =
    new ConfigurationProperty("", typeof(AddCollection), null, ConfigurationPropertyOptions.IsDefaultCollection);
 
  [ConfigurationProperty("view")]
  public string View
  {
    get
    {
      return base["view"].ToString();
    }
  }
 
  [ConfigurationProperty("handler")]
  public string Handler
  {
    get
    {
      return base["handler"].ToString();
    }
  }
 
  [ConfigurationProperty("", IsDefaultCollection = true)]
  public AddCollection AddCollection
  {
    get
    {
      return (AddCollection)base[_addProperty];
    }
  }    
}

add Node Set:


/// <summary>
/// add Node set 
/// </summary>  
public class AddCollection : ConfigurationElementCollection
{     
  /*override*/
 
  protected override ConfigurationElement CreateNewElement()
  {
    return new AddElement();
  }
 
  protected override object GetElementKey(ConfigurationElement element)
  {
    return element;
  }
}

add Node:


public class ExceptionConfig
{
  /// <summary>
  ///  View 
  /// </summary>
  public string View{get;set;}
 
  /// <summary>
  ///  Exception object 
  /// </summary>
  public Exception Exception{get;set;}
 
  /// <summary>
  ///  Exception handler 
  /// </summary>
  public IExceptionHandler Handler{get;set;}
}
0

STEP 3 Test

ok, test 1 below, first register our filter in the RegisterGlobalFilters method of FilterConfig before HandlerErrorAttribute:

filters. Add (new SettingHandleErrorFilter ()).

3.1 Prepare Exception Objects

Prepare a few simple exception objects:


public class ExceptionConfig
{
  /// <summary>
  ///  View 
  /// </summary>
  public string View{get;set;}
 
  /// <summary>
  ///  Exception object 
  /// </summary>
  public Exception Exception{get;set;}
 
  /// <summary>
  ///  Exception handler 
  /// </summary>
  public IExceptionHandler Handler{get;set;}
}
1

3.2 Prepare Handler

For the above exception, we prepare two Handler, one to handle password error exceptions and one to handle null exceptions. There is no actual processing code here, so how to deal with it should be combined with specific business. Such as:


public class ExceptionConfig
{
  /// <summary>
  ///  View 
  /// </summary>
  public string View{get;set;}
 
  /// <summary>
  ///  Exception object 
  /// </summary>
  public Exception Exception{get;set;}
 
  /// <summary>
  ///  Exception handler 
  /// </summary>
  public IExceptionHandler Handler{get;set;}
}
2

3.3 Throw an exception

According to the above configuration, we manually throw exception in Action


public ActionResult Index()
{
  throw new PasswordErrorException();
}
public ActionResult Index2()
{
  throw new UserNameEmptyException();
}
public ActionResult Index3()
{
  throw new EmailEmptyException();
}

As you can see, the corresponding Handler will be executed, and the browser will also have the error page we configured.

4. Summary

In fact, this is just a simple example, so I call it a simple module, but use words such as framework and library. Of course, we can expand and optimize it according to the actual situation. Microsoft Enterprise Library depends on the integration of such modules, and interested friends can understand 1.


Related articles: