Action Filters to Create Cleaner Code

Table of contents

During product development, software developers often face the problem of duplicate code. There are many ways to solve this problem. In this post, we will show how action filters can be used to clean up the code.

The role of filters in processing requests

Action filters allow for specific actions to be performed at various stages of request processing in ASP.NET Core. The following built-in filters exist:

  • Authorization filters are executed first and determine whether a user is allowed to complete the current request.
  • Resource filters are called after authorisation filters, and they are required, as the name suggests, to process resources. In particular, this type of filter is used as a caching mechanism.
  • Action filters perform the specified actions before and after the execution of the controller method that handles the request.
  • Exception filters are used to catch unhandled exceptions that occur during controller creation, model binding, and the execution of controller action filters and methods.
  • Finally, result filters are called if the controller method was successful. This type of filter is most commonly used to modify the final results; for example, developers can create their own response headers, in which they add extra information.

Below is a diagram showing the order in which the filters are called during request processing:

 the order in which the filters are called

Action filters can be considered the most useful in daily programming. With their help, a developer can take out repetitive code snippets and put them in one place. We will show examples of how these filters can be used, but first, let’s discuss the filters themselves.

Action filters under the hood

Action filters in ASP.NET

To create an action filter, a developer needs to implement the IActionFilter interface. This interface exists in ASP.NET MVC and defines the methods of OnActionExecuting, which is called before the controller method is executed, and OnActionExecuted, which is called immediately after. Below is an example of a simple action filter implementation that displays debug information before and after the controller method execution:

public class CustomActionFilter:IActionFilter 
{ 
        public void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            Debug.WriteLine("Before Action Execution"); 
        } 
        public void OnActionExecuted(ActionExecutedContext filterContext) 
        { 
            Debug.WriteLine("After Action Execution"); 
        } 
}

To use the filter above, it needs to be registered. To do this, add the following line to the FilterConfig.cs file located in the App_Start folder:

public static void RegisterGlobalFilters(GlobalFilterCollection filters) 
{ 
        filters.Add(new HandleErrorAttribute()); 
        filters.Add(new CustomActionFilter()); 
}

However, it is much more convenient to use filters as attributes. For these purposes, there is an abstract class ActionFilterAttribute that inherits the members from the FilterAttribute class and implements the IActionFilter and IResultFilter interfaces. Thus, the class above can be implemented as follows:

public class CustomActionFilterAttribute:ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            Debug.WriteLine("Before Action Execution"); 
        } 
        public override void OnActionExecuted(ActionExecutedContext filterContext) 
        { 
            Debug.WriteLine("After Action Execution"); 
        } 
} 

Now, to apply the filter, add it to the controller method this way:

public class HomeController : Controller 
{ 
        [CustomActionFilter] 
        public ActionResult Index() 
        { 
            return View(); 
        } 
}

This method also has a small advantage: a developer can apply filters to either a specific method or the entire controller and not register them globally.

Action filters in ASP.NET Core

There have been many changes to action filters with the introduction of ASP.NET Core. In addition to the IActionFilter interface, there is now an IAsyncActionFilter interface with a single OnActionExecutionAsync method. Below is an example of a class that implements the IAsyncActionFilter interface:

public class AsyncCustomActionFilterAttribute:Attribute, IAsyncActionFilter 
{ 
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 
        { 
            Debug.WriteLine("Before Action Execution"); 
            await next(); 
            Debug.WriteLine("After Action Execution"); 
        } 
} 

The ActionExecutionDelegate delegate is passed to the method as the second parameter. With its help, either the next action filters or the controller method itself are called.

The usage is the same as with the synchronous filter:

public class HomeController : Controller 
{ 
        [CustomActionFilter] 
        public ActionResult Index() 
        { 
            return View(); 
        } 
}

The abstract class ActionFilterAttribute has also been changed. Now, it is derived from the Attribute class and implements the synchronous and asynchronous interfaces of action filters (IActionFilter and IAsyncActionFilter) and result filters (IResultFilter and IAsyncResultFilter), as well as the IOrderedFilter interface.

Action filters in action

Now, let's discuss cases in which it is better to use action filters. For example, take a situation where a software engineer creates a web application and needs to save the data that the application receives through the POST method. Let's say the engineer is keeping information about the employees of an organisation. To represent the data on a server, the following class can be used:

public class Employee 
{ 
        [Required(ErrorMessage = "First name is required")] 
        public string FirstName { get; set; } 
        [Required(ErrorMessage = "Last name is required")] 
        public string LastName { get; set; } 
        [AgeRestriction(MinAge = 18, ErrorMessage = "Date of birth is incorrect")] 
        public DateTime DateOfBirth { get; set; } 
        [StringLength(50, MinimumLength = 2)] 
        public string Position { get; set; } 
        [Range(45000, 200000)] 
        public int Salary { get; set; } 
} 

With the help of validation attributes, a developer can control the correctness of the entered data. It should be noted that attributes with static parameters are not always the best way to validate data. For example, consider the fields indicating the age and salary of an employee in the example above. It would be better if a developer created a dedicated service that performed the validation of such fields, but in the scope of this article, let's use only validation attributes.

After the implementation of POST and PUT methods, it becomes clear that they both contain repeated code snippets:

[HttpPost] 
public IActionResult Post([FromBody] Employee value) 
{ 
            if (value == null) 
            { 
                return BadRequest("Employee value cannot be null"); 
            } 
            if (!ModelState.IsValid) 
            { 
                return BadRequest(ModelState); 
            } 
            // Perform save actions 
            return Ok(); 
} 
[HttpPut] 
public IActionResult Put([FromBody] Employee value) 
{ 
            if (value == null) 
            { 
                return BadRequest("Employee value cannot be null"); 
            } 
            if (!ModelState.IsValid) 
            { 
                return BadRequest(ModelState); 
            } 
            // Perform update actions 
            return Ok(); 
} 

This is where action filters come in. Let's create a new action filter and move duplicate code snippets to it as follows:

public class EmployeeValidationFilterAttribute : ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext context) 
        { 
            var employeeObject = context.ActionArguments.SingleOrDefault(p => p.Value is Employee); 
            if (employeeObject.Value == null) 
            { 
                context.Result = new BadRequestObjectResult("Employee value cannot be null"); 
                return; 
            } 
            if (!context.ModelState.IsValid) 
            { 
                context.Result = new BadRequestObjectResult(context.ModelState); 
            } 
        } 
} 

Now, the code snippets that have become redundant can be removed:

public class EmployeeController : ControllerBase 
{ 
        [EmployeeValidationFilter] 
        [HttpPost] 
        public IActionResult Post([FromBody] Employee value) 
        { 
            // Perform save actions 
            return Ok(); 
        } 
        [EmployeeValidationFilter] 
        [HttpPut] 
        public IActionResult Put([FromBody] Employee value) 
        { 
            // Perform update actions 
            return Ok(); 
        } 
} 

As a result, the code looks more compact and prettier, but in the current case, it can still be simplified. There are only two methods in the controller, and both use the same filter, so the attribute can be applied directly to the controller:

[EmployeeValidationFilter] 
public class EmployeeController : ControllerBase 
{ 
            // Perform update actions 
} 

Thus, with the help of action filters, the duplicate code snippets are removed. This process may look pretty straightforward, but what should users do they need to pass a dependency to an action filter?

Developers often face the task of adding logging for various methods. Therefore, let's try to add a logging tool to the action filters that will log information before executing the POST or PUT methods in the controller and immediately after. The filter will look like this:

public class LoggingFilter: IActionFilter 
{ 
        private readonly ILogger _logger; 
        public LoggingFilter(ILoggerFactory loggerFactory) 
        { 
            _logger = loggerFactory.CreateLogger<LoggingFilter>(); 
        } 
        public void OnActionExecuted(ActionExecutedContext context) 
        { 
            _logger.LogInformation($"{context.ActionDescriptor.DisplayName} executed"); 
        } 
        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            _logger.LogInformation($"{context.ActionDescriptor.DisplayName} is executing"); 
        } 
} 

Now, this filter can be applied either globally or to a specific section. Let's try to register it globally first. To do this, a user should add the following statements to Startup.cs:

services.AddControllers(options => 
{ 
                options.Filters.Add<LoggingFilter>(); 
}); 

If a developer needs to apply the filter, for example, to a specific controller method, then the filter should be used together with the ServiceFilterAttribute:

[HttpPost] 
[ServiceFilter(typeof(LoggingFilter))] 
public IActionResult Post([FromBody] Employee value) 

ServiceFilterAttribute is a factory for other filters that implement the IFilterFactory interface and uses IServiceProvider to obtain the proper filter. Therefore, the filter should be registered in Startup.cs as follows:

services.AddSingleton<LoggingFilter>(); 

After starting the application, we can make sure that the action filter is applied only to those controllers and methods in which it is specified as an attribute.

This method allows developers to create handy utility attributes that can be easily reused. Below is an example of a filter that checks for the existence of an object. The filter searches the object using its ID and returns it if the object exists:

public class ProviderFilter : IActionFilter 
{ 
        private readonly IDataProvider _dataProvider; 
        public ProviderFilter(IDataProvider dataProvider) 
        { 
            _dataProvider = dataProvider; 
        } 
        public void OnActionExecuted(ActionExecutedContext context) 
        { 
        } 
        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            object idValue; 
            if (!context.ActionArguments.TryGetValue("id", out idValue)) 
            { 
                throw new ArgumentException("id"); 
            } 
            var id = (int)idValue; 
            var result = _dataProvider.GetElement(id); 
            if (result == null) 
            { 
                context.Result = new NotFoundResult(); 
            } 
            else 
            { 
                context.HttpContext.Items.Add("result", result); 
            } 
        } 
} 

A developer can apply this filter in the same way as the filter in the previous example by using the ServiceFilterAttribute.

Action filters were often used to block content for specific browsers based on User-Agent information. In the early days of web development, many sites were created exclusively for the most popular browsers, while others were considered 'forbidden'. This approach is obsolete, and it is now recommended to create HTML markup that most browsers can support. However, in some cases, developers need to know the source of the request. Below is an example of how User-Agent information can be obtained in an action filter:

public class BrowserCheckFilter : IActionFilter 
{ 
        public void OnActionExecuting(ActionExecutingContext context) 
        { 
            var userAgent = context.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString().ToLower(); 
            // Detect if a user uses IE 
            if (userAgent.Contains("msie") || userAgent.Contains("trident")) 
            { 
                // Do some actions  
            } 
        } 
        public void OnActionExecuted(ActionExecutedContext context) 
        { 
        } 
} 

It is worth mentioning that the above method has another drawback. Many browsers know how to hide or fake the values specified in the User-Agent, so this method is somewhat unreliable in determining the type of user browser.

Another example of applying action filters is localisation. Let's create a filter that will display the date depending on the specified culture. Below is the code snippet that sets the culture of the current thread:

public class LocalizationActionFilterAttribute: ActionFilterAttribute 
{ 
        public override void OnActionExecuting(ActionExecutingContext filterContext) 
        { 
            var language = (string)filterContext.RouteData.Values["language"] ?? "en"; 
            var culture = (string)filterContext.RouteData.Values["culture"] ?? "GB"; 
            Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo($"{language}-{culture}"); 
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo($"{language}-{culture}"); 
        } 
} 

The next step is to add routing that will redirect the culture data URL to our controller:

                endpoints.MapControllerRoute(name:"localizedRoute", 
                    pattern: "{language}-{culture}/{controller}/{action}/{id}", 
                    defaults: new 
                    { 
                        language = "en", 
                        culture = "GB", 
                        controller = "Date", 
                        action = "Index", 
                        id = "", 
                    });

The code snippet above creates a route named localizedRoute, which has a localisation parameter in the template. The default value for this parameter is 'en-GB'.

Now, let's create a controller named DateController that will handle our request and a view that will display the localized date. The controller code simply returns the current date to the view:

[LocalizationActionFilter] 
public class DateController : Controller 
{ 
        public IActionResult Index() 
        { 
            ViewData["Date"] = DateTime.Now.ToShortDateString(); 
            return View(); 
        } 
}  

When a user opens the link https://localhost:44338/Date, they will see the following in the browser:

localized date

In the screenshot above, the current date is presented with the default localization; in our case, it is en-GB. Now, if a user opens a link that explicitly indicates the culture, such as en-US, they will see the following:

localized date

Closing

In conclusion, we should note that filters are just one of the many mechanisms that ASP.NET provides. Therefore, there is a high probability that the problem can be solved in other, more familiar, ways, like creating a service into which a developer can take out repetitive code. As you can see above, the great advantage of filters is that they are simple to implement and use. Therefore, they should be remembered as straightforward methods that can always be implemented in a project to keep the code clean.

You Might Also Like

Blog Posts Distribution of Educational Content within LMS and Beyond
October 16, 2023
When creating digital education content, it is a good practice to make it compatible with major LMSs by using one of the widely used e-learning standards. The post helps to choose a suitable solution with minimal compromise.
Blog Posts Story of Deprecation and Positive Thinking in URLs Encoding
May 13, 2022
There is the saying, ‘If it works, don’t touch it!’ I like it, but sometimes changes could be requested by someone from the outside, and if it is Apple, we have to listen.
Blog Posts The Laws of Proximity and Common Region in UX Design
April 18, 2022
The Laws of Proximity and Common Region explain how people decide if an element is a part of a group and are especially helpful for interface designers.