Creating Custom ASP.NET MVC Filters

ASP.NET MVC filters can be defined once and used in multiple places. Because ASP.NET MVC also provides the ability to create custom filters, it's good practice to learn how to make them and use them in your own projects.

ASP.NET MVC filters are declarative attributes that capture, or filter, a specific behavior on an action method or controller class. When applied, filters can trigger methods before, during or after an action occurs. This allows for added capabilities when handling actions, via default or custom filters.

The ASP.NET MVC framework offers several filters available by default. Examples of these are shown in Table 1.

Table 1: Model-View-Controller filters that are available by default.
Filter Name Description
[AuthorizeAttribute] This Authorization filter limits user access depending on authentication or authorization.
[HandleErrorAttribute] This Exception filter allows for custom exception handling of action methods.
[OutputCacheAttribute] This Result filter caches output for a specified duration.
[RequireHttpsAttribute] This Authorization filter requires HTTPS to be used instead of unsecured HTTP.

All filters, either default or custom, are defined by a class that inherits the base class System.Web.Mvc.FilterAttribute and implements a corresponding interface. Listed in Table 2 are the different types of ASP.NET MVC 5 filters, their corresponding interfaces and the methods available.

Table 2: The filter types available in ASP.NET MVC 5.
Filter Type Interface Methods
Authentication IAuthenticationFilter OnAuthentication On
Authorization IAuthorizationFilter OnAuthorization
Action IActionFilter OnActionExecuting On
Result IResultFilter OnResultExecuting
Exception IExceptionFilter OnException

A few notes about the default filters in ASP.NET MVC 5. First, the Authentication filter is newly introduced. Second, by design, Authorization filters run before any other filter. Action and Result filters are the most common types of filters. They've become so common that the ASP.NET MVC team has created an ASP.NET MVC base class System.Web.Mvc.ActionFilterAttribute. Last, the Exception filter is executed when an unhandled exception is thrown in a method decorated with the Exception filter attribute.

In addition to the various filters offered by default, ASP.NET MVC 5 also allows the ability for creating custom filters. This will be the basis for this article.

Sample Application
To demonstrate creating a custom filter, I'll use Visual Studio 2013 and the ASP.NET MVC 5 template. I'll create a new custom Exception filter type. This new filter will catch unhandled exceptions and process them accordingly. The intention is to have the filter call a specific routine depending on the type of exception thrown.

Upon starting Visual Studio 2013, you'll notice the Web templates have been reorganized and all categorized as ASP.NET Web Application, as shown in Figure 1.

[Click on image for larger view.] Figure 1. Web Templates Categorized as ASP.NET Web Application

After specifying the name and location of the new solution to be created (CustomMVCFilterDemo), clicking OK will display the selection window shown in Figure 2. It's in this window where the specific project template will be selected.

[Click on image for larger view.] Figure 2. Selecting the Specific Project Template

For this demo, I chose the MVC template without adding a unit test project. Clicking the Change Authentication button reveals another window with a menu of authentication options, as shown in Figure 3.

[Click on image for larger view.] Figure 3. Authentication Options

For the purposes of this sample application, I selected No Authentication.

With the default project template created, the next step is to add a Filters folder. This isn't necessary but it'll help keep the solution organized, following best practices. Within the newly created folder, I'll create a new class called CustomExceptionFilter.vb.

As stated previously, all filters inherit from the System.Web.MVC.FilterAttribute base class. Therefore, the new CustomExceptionFilter class will inherit it. In addition, it will also implement the IExceptionFilter interface. Part of this implementation includes defining the OnException method, which I did. I also created a WriteLog method that will be used for displaying messages in the text-based log file (C:\CustomMVCFiltersDemo\ExceptionLog.txt). The complete code listing for CustomExceptionFilter.vb can be seen in Listing 1.

Listing 1: CustomExceptionFilter.vb
Imports System.IO
Imports System.Web.Mvc
Imports System.Reflection.MethodBase

Public Class CustomExceptionFilter
  Inherits System.Web.Mvc.FilterAttribute
  Implements IExceptionFilter

  Public Sub OnException(
    filterContext As ExceptionContext) Implements IExceptionFilter.OnException
    Dim LogLine As String = String.Empty

    'Gather filterContext info for logging
    Dim ExMessage As String = filterContext.Exception.Message
    Dim ExType As System.Type = filterContext.Exception.GetType
    Dim ExName As String = ExType.ToString()

    Select Case (ExType)
      Case GetType(EntitySqlException)

      Case GetType(OverflowException)
    End Select

    'Gather RouteData info for logging
    Dim RtData As RouteData = filterContext.RouteData
    Dim ControllerClassName As String = RtData.Values("controller")
    Dim ActionMethodName As String = RtData.Values("action")

    Dim MethodName As String = GetCurrentMethod().Name

    LogLine = String.Format(
      "Logging MethodName={0}; Exception Message={1}; ControllerClass={2};  ActionMethod={3}", 
      MethodName, ExMessage, ControllerClassName, ActionMethodName)

  End Sub

  Private Sub NotifyHelpDesk(Msg As String)

    Dim MethodName As String = GetCurrentMethod().Name
    WriteLog(String.Format("Logging MethodName={0}; Exception Message={1}", MethodName, Msg))

    'Include additional logic here to notify help desk via SMS or other means
  End Sub

  Private Sub NotifyDevTeam(Msg As String)

    Dim MethodName As String = GetCurrentMethod().Name
    WriteLog(String.Format("Logging MethodName={0}; Exception Message={1}", MethodName, Msg))

    'Include additional logic here to notify Dev team via e-mail or other means
  End Sub

  Public Sub WriteLog(Msg As String)
    Dim sw As StreamWriter = New StreamWriter("C:\CustomMVCFiltersDemo\ExceptionLog.txt", True)
    sw.WriteLine(DateTime.Now.ToString() + ": " + Msg)
  End Sub

End Class

Looking at OnException in more detail, it accepts an ExceptionContext type parameter. This object, as the name suggests, provides a variety of information about the exception and its location (type, Controller Class, Action Method and so on). To learn more about ExceptionContext, please see the MSDN documentation here.

I'm using the ExceptionContext object in OnException to extract more information for logging. I also added a code segment to simulate notifying different teams depending on the type of exception. For example, if the exception is type EntitySqlException, then it will call NotifyHelpDesk. If the exception is of type OverflowException, then it will call NotifyDevTeam. Both NotifyHelpDesk and NotifyDevTeam simply log the error, but they also act as place holders for code to show how different routines can be called from OnException.

To use the newly created Exception filter, I'll apply the attribute <CustomExceptionFilter> 
to the HomeController Class, in HomeController.vb, as seen in Listing 2.
Listing 2: HomeController.vb
Imports CustomMVCFiltersDemo.CustomExceptionFilter
Imports System.Reflection.MethodBase

Public Class HomeController
  Inherits System.Web.Mvc.Controller

  Function Index() As ActionResult
    Return View()
  End Function

  Function About() As ActionResult
      Throw New EntitySqlException("Throwing SQL Exception from About()")
      ViewData("Message") = "Your application description page."

      'Handling the exception locally, then re-throwing it to have 
      'OnException process it as well.
    Catch Ex As Exception
      Dim csf As New CustomExceptionFilter()  'This is needed only to access WriteLog()
      Dim ec As New ExceptionContext()

      Dim MethodName As String = GetCurrentMethod().Name
        "Logging MethodName={0}; Exception Message={1}", MethodName, Ex.Message))

      Throw Ex  'This is required to ensure OnException gets called.
    End Try

    Return View()

  End Function

  Function Contact() As ActionResult

    Dim a, b, c As Int32
    a = 3
    b = 0
    c = (a / b)  'this intended to throw an OVERFLOW exception

    ViewData("Message") = "Your contact page."
    Return View()

  End Function

End Class

If you look closely at the About and Contact methods, you'll see I inserted some code to deliberately cause an exception in each one. Only About has a Try-Catch block for handling the error. This is used only for demonstration purposes and by no means do I consider this production code.

When I execute the app from Visual Studio, the default homepage is launched, as shown in Figure 4.

[Click on image for larger view.] Figure 4. The Default ASP.NET MVC 5 Homepage.

Clicking on About will call the About action method where an EntitySqlException is thrown. The Try-Catch block will catch the error, calling WriteLog to log the error in the log file. Within the Try-Catch block, the error is thrown again. This will cause a Visual Studio pop-up error message to appear, as shown in Figure 5. If the application was running in IIS, not Visual Studio, you wouldn't see the pop-up error messages.

[Click on image for larger view.] Figure 5. EntitySqlException Pop-Up Error Message in Visual Studio

Clicking Continue in Visual Studio will allow OnException to be called. Remember, in order for the exception filter to catch an exception, it has to be unhandled. By throwing the error from within the Try-Catch block, I'm essentially creating an unhandled exception. After continuing, an error message will appear on the Web page. Figure 6 shows the error message with the exception message highlighted.

[Click on image for larger view.] Figure 6. Error Message Showing the Exception Message Thrown from the About Method

Looking at the ExceptionLog.txt, you'll see the entries generated from this exception:

12/11/2013 4:37:43 PM: Logging MethodName=About; Exception Message=Throwing SQL Exception from About()
12/11/2013 4:37:46 PM: Logging MethodName=NotifyHelpDesk; Exception Message=Throwing SQL Exception from About()
12/11/2013 4:37:46 PM: Logging MethodName=OnException; Exception Message=Throwing SQL Exception from About(); ControllerClass=Home;  ActionMethod=About

The first entry is logged by the method About, as specified in the Logging MethodName field. This is because About has a Try-Catch block that captures the exception and calls WriteLog to log the error. The second line is an entry logged by NotifyHelpDesk. The third entry is logged by OnException. This is due to OnException calling NotifyHelpDesk if the Exception is an EntitySqlException type. NotifyHelpDesk logs the error using WriteLog and displays only the Exception Message. Afterward, OnException logs the error, showing additional information (ControllerClass, ActionMethod)

Next, I'll focus on testing the Contact action method. To do so, I'll restart the application, and from the default homepage, click on Contact. This immediately renders a Visual Studio pop-up error message, shown in Figure 7, where a divide-by-zero operation caused an OverflowException to be thrown.

[Click on image for larger view.] Figure 7. The OverflowException Pop-Up Error Message

Once again, clicking Continue in Visual Studio causes the error to surface on the Web page (see Figure 8).

[Click on image for larger view.] Figure 8. The Exception Message Thrown from Contact

The log file shows two additional entries generated:

12/11/2013 4:41:05 PM: Logging MethodName=NotifyDevTeam; Exception Message=Arithmetic operation resulted in an overflow.
12/11/2013 4:41:05 PM: Logging MethodName=OnException; Exception Message=Arithmetic operation resulted in an overflow.; ControllerClass=Home;  ActionMethod=Contact

This is in comparison to the three entries generated by clicking About. The action method Contact doesn't have a Try-Catch block to log the error. Therefore, NotifyException calls NotifyDevTeam, which logs the error. The next entry is logged by OnException, and then the process terminates as expected.

The Filter Advantage The advantage of ASP.NET MVC filters is they can be defined once, and used in multiple places. Defining a custom filter is simplified through the use of base class inheritance. By using the name of the custom filter object as an attribute, you can decorate either a class or an individual method to filter certain behavior. In addition, the ExceptionContext object provides all the details of the exception for logging or invoking custom logic. This makes filters a valuable feature in ASP.NET MVC.

comments powered by Disqus


Subscribe on YouTube