Creating Global Filters - Code Architecture - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part III. Code Architecture

Chapter 9. Creating Global Filters

Global filters enable you to apply a consistent behavior across all requests to your web application by registering a filter during the application startup. Filters can also be applied to specific actions or entire controllers by adding an attribute to the action or controller, respectively.

Five different types of filters can be created. At the start of each web request, any filter that is defined is executed in the following order — the exception to this rule is the Exception filter (no pun intended) because these filters are only called when an error occurs:

§ Authentication filters (new in MVC 5)

§ Authorization filters

§ Action filters

§ Result filters

§ Exception filters

This chapter will provide a brief overview of all five types of filters and then will demonstrate how to create Action, Result, and Exception filters. Chapter 10 will demonstrate how to create Authentication and Authorization filters.

Authentication Filters

Authentication filters are new to MVC 5. Prior to that, authentication and authorization were accomplished together in the Authorization filters. MVC 5 has now separated these two concerns.

When MVC receives a web page request, any Authentication filters will be executed first. If the request requires authentication and the user has previously been authenticated, the request will continue to the next step. If the user has not been authenticated, the request will halt processing. Based on the setup of the filter, the request may redirect the user to a login page; this is commonly done with an MVC controller. A Web API controller would more likely return a 401 Unauthorized request.

Authorization Filters

Once the request has passed any Authentication filters, the Authorization filters are executed next. The goal of an Authorization filter is to ensure that the authenticated user is allowed to access the page or resource being requested. If authorization succeeds, the request will continue to the next step. If it fails authorization, MVC controllers commonly return error pages. A Web API controller would commonly return a 403 Forbidden request. Alternatively, it may return a 404 Not Found error and inform the user that the resource being accessed does not exist, even though it actually does.

Action Filters

Action filters provide the ability to execute code at two different times. When you define an Action filter, you can optionally implement a function that executes prior to the action being requested, or optionally after the action has finished executing, but prior to generating the final results to complete the request.

Result Filters

Like Action filters, Result filters provide two different functions that can be optionally implemented. The first is when the result has finished executing; for example, in an MVC controller once the view has been fully rendered and is ready to be returned from the server. The second is when the result is executing. This function would not have access to the final content.

Exception Filters

At any time during a request being handled by MVC, any Exception filters will be executed. Filters for MVC controllers will commonly return a custom error page that can return either a specific or generic error message. Web API controllers will commonly build different HTTP status codes. For example, if the model is invalid, a 400 Bad Request could be returned. If an unknown exception occurred, a 500 error would be more appropriate to indicate a server error occurred.

Global Web API Validation

When Visual Studio scaffolds controllers and views for us, the controller contains a statement inside the Create and Edit methods that resembles Example 9-1.

Example 9-1. Model validation

if (!ModelState.IsValid)

{

return BadRequest(ModelState);

}

The ModelState is a class that inherits from a Dictionary that contains key/value pairs for all of the elements in the model being validated. When the IsValid Boolean returns false, it indicates one or more things are invalid in the model and cannot be saved.

In a Web API controller, this means it should return a 400 Bad Request with a message indicating the issues that need to be fixed.

This section will create a new class called ValidationActionFilterAttribute (shown in Example 9-2). I like to organize my filters in a common folder, so I have created a new folder called Filters at the root of my project and created the Action filter within it.

Example 9-2. ValidationActionFilterAttribute

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Net.Http;

using System.Web.Http.Controllers;

using System.Web.Http.Filters;

namespace BootstrapIntroduction.Filters

{

public class ValidationActionFilterAttribute : ActionFilterAttribute

{

public override void OnActionExecuting(HttpActionContext actionContext)

{

var modelState = actionContext.ModelState;

if (!modelState.IsValid)

actionContext.Response = actionContext.Request.CreateResponse(

HttpStatusCode.BadRequest, modelState);

}

}

}

The new Action filter inherits from the ActionFilterAttribute, which is an abstract class that contains four virtual functions that can be optionally overridden in your Action filter class:

OnActionExecuting

The OnActionExecuting function is called just prior to executing the code inside of your controller method. As shown in Example 9-2, if the ModelState is invalid, the response is immediately terminated and a 400 Bad Request is returned from the server. This ensures that your data is valid when your controller method is executed based upon the validation rules of that model.

OnActionExecutingAsync

The OnActionExecutingAsync function is identical to the OnActionExecuting function with the exception that it works with asynchronous controllers.

OnActionExecuted

The OnActionExecuted function is called after your controller method has finished executing, but it is extremely important that it is triggered prior to the response being constructed and sent back to the server.

OnActionExecutedAsync

The OnActionExecutedAsync function is identical to the OnActionExecuted function with the exception that it works with asynchronous controllers.

Once the ValidationActionFilterAttribute is created, it can be implemented globally so that none of the API controllers need to perform the same validation inside of each method.

Global Web API filters are defined in the WebApiConfig class that was created in Chapter 8 when we installed Web API. Example 9-3 contains an updated WebApiConfig class that registers the ValidationActionFilterAttribute.

Example 9-3. Updated WebApiConfig

using BootstrapIntroduction.Filters;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web.Http;

namespace BootstrapIntroduction

{

public static class WebApiConfig

{

public static void Register(HttpConfiguration config)

{

// Web API configuration and services

config.Filters.Add(new ValidationActionFilterAttribute());

// Web API routes

config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(

name: "DefaultApi",

routeTemplate: "api/{controller}/{id}",

defaults: new { id = RouteParameter.Optional }

);

}

}

}

Testing the validation is not quite so straightforward. When the authors forms were initially created, they were configured to perform client-side validation to avoid unnecessary requests to the server to perform the same validation.

You can disable this or use a free tool called Fiddler from Telerik to perform a direct request to the authors Web API controller.

Installing Fiddler

You can install Fiddler by visiting Telerik’s Fiddler product page and clicking the Free Download button.

Fiddler currently offers an Alpha version for Linux and Mac. Of course, any traffic-monitoring software can be used to perform this test if you prefer not to use Fiddler.

If your web application is not running, be sure it is running now. With Fiddler open, you’ll see a handful of tabs near the top right-hand side. In this list is a tab called Composer, which allows you to execute your own web request.

Figure 9-1 contains the setup I used to execute a request to create an author against the REST API previously created.

Figure 9-1. Composer settings

There are several key settings required to execute the request:

§ A content-type. I used application/json.

§ The request type. This must be POST. This is selected from the drop-down beside the URL.

§ The URL, it might be slightly different from mine if the random port differs. It’s important that you use /api/authors at the end of the URL.

§ The request body. I set it to {}, which is JSON syntax for an empty request body.

Finally, you can execute the request by clicking the Execute button near the upper right.

Once your request is executed, it will appear on the left with any other requests that Fiddler is currently monitoring. Find your request and double-click to select it and view the results.

As expected, this request failed with a 400 Bad Request. In Fiddler, you can select the JSON view to see the results returned from the server. Figure 9-2 contains the response from my failed call.

Figure 9-2. 400 Bad Request

Each key in the JSON contains the field that has a validation issue. Inside the field is an array of errors that contains a specific error message that can be used to inform the API integrator why the request is invalid.

Automapping with a Result Filter

In the previous section, the Action filter was added globally to all Web API requests. However, it doesn’t make sense to apply all filters globally. Filters can also be added directly to one or more methods in your controller, or if it makes sense, to the entire controller itself.

This section will demonstrate how this is done by creating a custom Result filter. The Result filter will update the Index function of the AuthorsController to not perform the Automapping and creation of the ResultList. The Index view will still depend on this; however, as you create more and more controllers with a listing page of the model, the generation of the ResultList will quickly become extremely repetitive.

Example 9-4 contains a new class called GenerateResultListFilterAttribute, and I have placed it within the previously created Filters folder.

Example 9-4. GenerateResultListFilterAttribute

using BootstrapIntroduction.Models;

using BootstrapIntroduction.ViewModels;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Web;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

[AttributeUsage(AttributeTargets.Method)]

public class GenerateResultListFilterAttribute : FilterAttribute, IResultFilter

{

private readonly Type _sourceType;

private readonly Type _destinationType;

public GenerateResultListFilterAttribute(Type sourceType, Type destinationType)

{

_sourceType = sourceType;

_destinationType = destinationType;

}

public void OnResultExecuting(ResultExecutingContext filterContext)

{

var model = filterContext.Controller.ViewData.Model;

var resultListGenericType = typeof(ResultList<>)

.MakeGenericType(new Type[] { _destinationType });

var srcGenericType = typeof(List<>).MakeGenericType(

new Type[] { _sourceType });

var destGenericType = typeof(List<>).MakeGenericType(

new Type[] { _destinationType });

AutoMapper.Mapper.CreateMap(_sourceType, _destinationType);

var viewModel = AutoMapper.Mapper.Map(model, srcGenericType, destGenericType);

var queryOptions = filterContext.Controller.ViewData.ContainsKey(

"QueryOptions") ?

filterContext.Controller.ViewData["QueryOptions"] :

new QueryOptions();

var resultList = Activator.CreateInstance(resultListGenericType, viewModel,

queryOptions);

filterContext.Controller.ViewData.Model = resultList;

}

public void OnResultExecuted(ResultExecutedContext filterContext)

{

}

}

}

This class contains some similarities to the previously created ValidationActionFilterAttribute with a few notable differences. The Result filter extends the base FilterAttribute class, and it implements the IResultFilter interface.

Implementing the IResultFilter requires two functions: OnResultExecuting and OnResultExecuted. These functions are quite similar to the Action filter equivalents in that the first one is called prior to the view being generated, and the second is called after the view is generated and ready to be returned to the server.

This Result filter only implements the OnResultExecuting function because it changes the model that was bound to the View from the Controller to a new ResultList model.

The GenerateResultListFilterAttribute expects two input parameters: sourceType and destinationType. These two type properties are used to perform the automapping from the data model to the ViewModel. The class is also attributed with an AttributeUsage that indicates this filter can only be used on methods. This is done because of the specific requirements for the constructor.

Inside the OnResultExecuting function, reflection is used to dynamically instantiate the ResultList to the destinationType and populate the results by executing the automapper.

Previously, in the Index function, the QueryOptions were passed to the View in the ViewBag before moving within the ResultList class. This Result filter assumes the QueryOptions will be stored in a similar ViewData dictionary that is accessed via the Result filter and passed to the ResultListclass.

A few more changes are required to make this work. Previously, the properties in the ResultList class were being publicly set; this has been updated to accept them via the constructor. Example 9-5 contains an updated ResultList class.

Example 9-5. Updated ResultList

using Newtonsoft.Json;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

namespace BootstrapIntroduction.ViewModels

{

public class ResultList<T>

{

public ResultList(List<T> results, QueryOptions queryOptions)

{

Results = results;

QueryOptions = queryOptions;

}

[JsonProperty(PropertyName="queryOptions")]

public QueryOptions QueryOptions { get; private set; }

[JsonProperty(PropertyName = "results")]

public List<T> Results { get; private set; }

}

}

This change will break how the Web API AuthorsController was previously instantiating the ResultList class. Example 9-6 contains an updated Index function to match the change.

Example 9-6. Updated Web API AuthorsController

public ResultList<AuthorViewModel> Get([FromUri]QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return new ResultList<AuthorViewModel>(

AutoMapper.Mapper.Map<List<Author>,

List<AuthorViewModel>>(authors.ToList()), queryOptions);

}

And finally, it’s time to update the MVC AuthorsController to leverage the Result filter and remove the now unneeded Automapping code from the Index function. Example 9-7 contains an updated Index function implementing the Result filter.

Example 9-7. Updated MVC AuthorsController

[GenerateResultListFilterAttribute(typeof(Author), typeof(AuthorViewModel))]

public ActionResult Index([Form] QueryOptions queryOptions)

{

var start = (queryOptions.CurrentPage - 1) * queryOptions.PageSize;

var authors = db.Authors.

OrderBy(queryOptions.Sort).

Skip(start).

Take(queryOptions.PageSize);

queryOptions.TotalPages =

(int)Math.Ceiling((double)db.Authors.Count() / queryOptions.PageSize);

ViewData["QueryOptions"] = queryOptions;

return View(authors.ToList());

}

Web API Error Handling

Exceptions happen all the time. Sometimes, it can be an unexpected exception; other times, it is a business validation exception. No matter what type of exception it is, a global Exception filter will help you to deal with all exceptions in a consistent fashion.

Example 9-8 contains a new OnApiExceptionAttribute class. This class can be created in the Filters directory. The goal of this class is to build a new HttpResponseMessage with a specific HTTP Status Code based on the type of exception that occurred. The content being returned will also be tailored to suppress unknown server errors.

Example 9-8. OnApiExceptionAttribute

using BootstrapIntroduction.ViewModels;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Net.Http;

using System.Web;

using System.Web.Http.Filters;

namespace BootstrapIntroduction.Filters

{

public class OnApiExceptionAttribute : ExceptionFilterAttribute

{

public override void OnException(HttpActionExecutedContext actionExecutedContext)

{

var exceptionType = actionExecutedContext.Exception.GetType().Name;

ReturnData returnData;

switch (exceptionType)

{

case "ObjectNotFoundException":

returnData = new ReturnData(HttpStatusCode.NotFound,

actionExecutedContext.Exception.Message, "Error");

break;

default:

returnData = new ReturnData(HttpStatusCode.InternalServerError,

"An error occurred, please try again or contact the administrator.",

"Error");

break;

}

actionExecutedContext.Response =

new HttpResponseMessage(returnData.HttpStatusCode)

{

Content = new StringContent(returnData.Content),

ReasonPhrase = returnData.ReasonPhrase

};

}

}

}

Creating an Exception filter involves creating a class that inherits from the ExceptionFilterAttribute class. Then you override the OnException function and add your custom logic.

Example 9-8 does this, and a switch statement is implemented to create a different type of ReturnData object (created in Example 9-9) based on the exception that occurred. To start, the switch statement only contains two cases. The first one is when the exception is anObjectNotFoundException, which will return a 404 Not Found exception and set the content of the response to the message within the exception. The second is the default case statement, which will return a 500 Internal Server Error. Here the content is set to a generic message to suppress what the actual error was.

As your code expands and you work with new exceptions, this switch statement can be extended to return many other different HTTP Status Codes and error content.

The OnApiExceptionAttribute leverages a new ViewModel called ReturnData. Example 9-9 contains the class definition. This file can be created in the ViewModels directory.

Example 9-9. ReturnData ViewModel

using System.Net;

namespace BootstrapIntroduction.ViewModels

{

public class ReturnData

{

public ReturnData(HttpStatusCode httpStatusCode, string content,

string reasonPhrase)

{

HttpStatusCode = httpStatusCode;

Content = content;

ReasonPhrase = reasonPhrase;

}

public HttpStatusCode HttpStatusCode { get; private set; }

public string Content { get; private set; }

public string ReasonPhrase { get; private set; }

}

}

Example 9-10 demonstrates how an ObjectNotFoundException can be thrown by implementing a new function in the Web API AuthorsController.

Example 9-10. Updated Web API AuthorsController

// GET: api/Authors/5

[ResponseType(typeof(AuthorViewModel))]

public IHttpActionResult Get(int id)

{

Author author = db.Authors.Find(id);

if (author == null)

{

throw new System.Data.Entity.Core.ObjectNotFoundException

(string.Format("Unable to find author with id {0}", id));

}

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return Ok(AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

The final piece of the puzzle is to add your new Exception Attribute to the WebApiConfig. Example 9-11 contains an updated WebApiConfig that instantiates the new OnApiExceptionAttribute.

Example 9-11. Updated WebApiConfig

using BootstrapIntroduction.Filters;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web.Http;

namespace BootstrapIntroduction

{

public static class WebApiConfig

{

public static void Register(HttpConfiguration config)

{

// Web API configuration and services

config.Filters.Add(new ValidationActionFilterAttribute());

config.Filters.Add(new OnApiExceptionAttribute());

// Web API routes

config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(

name: "DefaultApi",

routeTemplate: "api/{controller}/{id}",

defaults: new { id = RouteParameter.Optional }

);

}

}

}

To see this in action, with your web application running, you can navigate to this URL in your web browser: http://localhost:50955/api/authors/-2. This will return the error message “Unable to find author with id -2.” Please note that your URL might be slightly different if the port 50955 does not match.

MVC Error Handling

Creating an error handler for MVC is quite similar to creating one for Web API. Example 9-12 contains a new OnExceptionAttribute (no “Api” this time) that contains very similar logic to the OnApiExceptionAttribute.

Example 9-12. OnExceptionAttribute

using BootstrapIntroduction.ViewModels;

using System;

using System.Net;

using System.Web.Mvc;

namespace BootstrapIntroduction.Filters

{

public class OnExceptionAttribute : HandleErrorAttribute

{

public override void OnException(ExceptionContext exceptionContext)

{

var exceptionType = exceptionContext.Exception.GetType().Name;

ReturnData returnData;

switch (exceptionType)

{

case "ObjectNotFoundException":

returnData = new ReturnData(HttpStatusCode.NotFound,

exceptionContext.Exception.Message, "Error");

break;

default:

returnData = new ReturnData(HttpStatusCode.InternalServerError,

"An error occurred, please try again or contact the administrator.",

"Error");

break;

}

exceptionContext.Controller.ViewData.Model = returnData.Content;

exceptionContext.HttpContext.Response.StatusCode =

(int)returnData.HttpStatusCode;

exceptionContext.Result = new ViewResult

{

ViewName = "Error",

ViewData = exceptionContext.Controller.ViewData

};

exceptionContext.ExceptionHandled = true;

base.OnException(exceptionContext);

}

}

}

The OnExceptionAttribute extends the HandleErrorAttribute, and it overrides the OnException method. The first half of Example 9-12 is an identical switch statement that will set up the ReturnData object. After this is done, the result that was going to be displayed is altered to return an error view instead.

First, the ViewModel that is bound to a View is updated to write the Content from the ReturnData. This will be used by the Error view shown in Example 9-13. Next, the StatusCode of the Response is changed (quite similarily to how it was changed in the OnApiExceptionAttribute). And finally, a new ViewResult is created that will load the Error.cshtml view that exists within the Views/Shared folder.

After the result has been updated, the exception is marked as handled before calling the base OnException function.

Example 9-13 contains an updated Error.cshtml view from the Shared views folder.

Example 9-13. Updated Error.cshtml

@model string

<!DOCTYPE html>

<html>

<head>

<meta name="viewport" content="width=device-width" />

<title>Error</title>

</head>

<body>

<hgroup>

<h1>Error.</h1>

<h2>An error occurred while processing your request.</h2>

<p>@Model</p>

</hgroup>

</body>

</html>

Three minor changes have been made to the default error page. First, a ViewModel of type string has been bound to the view. Second, the Layout = null has been removed, so it will use the shared layout, and the error page will look like the rest of the site. And finally, the ViewModel that is bound to the view is displayed beneath the error headers inside a paragraph tag.

When the AuthorsController was first scaffolded, a Details function was created that accepts an author ID and will display information about the author. Example 9-14 updates this function to throw an ObjectNotFoundException if the author is null (just like Example 9-10 did for the Web API controller).

Example 9-14. Updated MVC AuthorsController

// GET: Authors/Details/5

public ActionResult Details(int? id)

{

if (id == null)

{

return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

}

Author author = db.Authors.Find(id);

if (author == null)

{

throw new System.Data.Entity.Core.ObjectNotFoundException

(string.Format("Unable to find author with id {0}", id));

}

AutoMapper.Mapper.CreateMap<Author, AuthorViewModel>();

return View(AutoMapper.Mapper.Map<Author, AuthorViewModel>(author));

}

And finally, the OnExceptionAttribute needs to be registered in the FilterConfig class as shown in Example 9-15.

Example 9-15. Updated FilterConfig

using BootstrapIntroduction.Filters;

using System.Web;

using System.Web.Mvc;

namespace BootstrapIntroduction

{

public class FilterConfig

{

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

{

filters.Add(new HandleErrorAttribute());

filters.Add(new OnExceptionAttribute());

}

}

}

To see the new error handler in action (as shown in Figure 9-3), you can visit http://localhost:50955/authors/Details/-2 in your browser.

Figure 9-3. Custom error handler

Summary

This chapter demonstrated three of the five different types of global filters you can create. Most of the filters were added globally, but as the Result filter demonstrated, filters can also be applied to individual actions.

MVC also contains many built-in Result and Action filters that you can use. Visit the MSDN description of the FilterAttribute class to see many of the existing Result filters. Likewise, on MSDN you can visit the ActionFilterAttribute class to see many of the existing Action filter attributes.