.NET Nakama

Improving your .NET skills

Creating and Testing ASP.NET Core Filter Attributes

March 04, 2021 (~20 Minute Read)
BASICS FILTER ATTRIBUTE ASP.NET CORE FILTER PIPELINE TESTING

Introduction

Filters are a great way to inject code in the filter-pipeline (aka invocation pipeline), in which the flow is based on the execution order of the different filter-types. Also, we can extract repetitive code out of the action methods. By this way, we can follow the Don’t Repeat Yourself (DRY) principle by creating a common abstraction.

In our previous article .NET Nakama (2021, February), we have investigated the filter pipeline, the different filter types and the cases in which can be used, and how the different filter-types can be applied in controllers and action methods. These concepts are used in the current article. Therefore, you can begin from the previous article (if you haven’t read it yet 😉).

In this article, we will start by investigating the difference between the implementation of synchronous and asynchronous filters. Then, we will see the questions that can guide us to make the appropriate decisions when implementing filter attributes. Finally, we will create two filter attributes (as examples) and unit tests for some of their scenarios-behaviour.

Synchronous and Asynchronous Filters

ASP.NET Core supports both synchronous and asynchronous filters. The main difference when implementing such filters is the selection of the appropriate interface.

Synchronous filter interfaces define two methods that are executed before and after a specific stage in the filter pipeline. The method that is executed before has a name that ends with Executing and the method that is executed after has a name that ends with Executed.

using Microsoft.AspNetCore.Mvc.Filters;

namespace CustomFilter
{
    public class MyActionFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // Do something before the action executes.
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // Do something after the action executes.
        }
    }
}

Asynchronous filters define one method with a name that ends with ExecutionAsync. The implementation is similar to a middleware’s implementation, in the sense that a next() method is used to execute any subsequent filters or action methods.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;

namespace CustomFilter
{
    public class SampleAsyncActionFilter : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(
            ActionExecutingContext context,
            ActionExecutionDelegate next)
        {
            // Do something before the action executes.


            // Execute any subsequent filters or action method.
            var executedContext = await next();


            // Do something after the action executes.
        }
    }
}

Decisions when Implementing Filters

We have already seen the different filter types and how to apply them in .NET Nakama (2021, February). Also, we have seen that filters support both synchronous and asynchronous implementations through different interface definitions, which are described in each filter type section.

Before starting implementing a filter, we can answer the following questions, which will guide us to make the appropriate decisions depending on the current goal.

  1. The filter should be synchronous or asynchronous?
    • Consider using asynchronous filters if there is a need for data access, I/O, and long-running operations.
    • Implement either the synchronous or the asynchronous version of a filter interface, but not both.
  2. Which filter type(s) are necessary for the current goal?
    • Depending on the implementation goal and by considering Figure 1, decide on which stage in the filer pipeline your code should be injected.
    • As a result, you should have decided the filter type(s) and the before-after methods that should be implemented.
  3. Does the filter depend on other services via Dependency Injection (DI)?
  4. How many filter types have been selected (one or many)?
    • Note: This question is applicable only when DI is not required.
    • If only one filter type is selected, consider using a built-in filter attribute depending on the selected filter type.
    • In case of multiple selected filter types, consider using the IFilterFactory, in which multiple filter types can be created in the same class.
The execution flow on the filter pipeline for the different filter types.
Figure 1. - The execution flow on the filter pipeline for the different filter types (Source).

After answering the previous questions, we should have decided the necessities to create a scaffold filter, which are:

  1. The filter type(s) that should be used.
  2. The before or/and after methods.
  3. The Interfaces or base class that should be implemented.

Creating Filters

We will implement two filter attributes (as an example) based on some needs (our goals) which will be used in a Web API project. You can download the source code of these examples from GitHub.

Create Filter by Using a Built-In Filter Attribute

Let’s assume that we need to return different HTTP-headers depending on the controller and the action method. We can start by answering the preparation questions:

Question Answer
The filter should be synchronous or asynchronous? Synchronous. Because it’s a simple task, without the need for data access, I/O, or long-running operations.
Which filter type(s) are necessary for the current goal? The Result filter type can be used, as we need to modify the result that is produced from the action method, before its execution (i.e. using the OnResultExecuting method).
Does the filter depend on other services via Dependency Injection (DI)? No, we do not need DI.
How many filter types have been (one or many)? We have selected one filter type (Result Filter). So, the ResultFilterAttribute built-in filter attribute can be used.

Based on our answers, we can create a synchronous ResultFilter that would be based on the ResultFilterAttribute class, which will be used to add the HTTP-headers in our response (AddHeaderAttribute.cs).

using Microsoft.AspNetCore.Mvc.Filters;

namespace CustomFilter
{
    /// <summary>
    /// Adds an HTTP Header to the Response, based on the input parameters.
    /// </summary>
    public class AddHeaderAttribute : ResultFilterAttribute
    {
        private readonly string _name;
        private readonly string[] _value;

        public AddHeaderAttribute(string name, string value)
        {
            _name = name;
            _value = new string[] { value };
        }

        public AddHeaderAttribute(string name, string[] value)
        {
            _name = name;
            _value = value;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(_name, _value);
        }
    }
}

Create Filter by Using the IFilterFactory and IExceptionFilter

In this example, we will use the IFilterFactory to demonstrate the creation of a class of multiple filter types and to investigate the filter pipeline flow in practice. In your case, you can select the number of filter types based on your needs.

Our filter attribute will be used to add a log each time a different filter type event is called. Additionally, we will implement an Exception filter, which will either redirect to a web-page or it will return a JSON response, depending on the Accept header.

The IFilterFactory interface and the Attribute base class is used to create the filter attribute (MyMultipleActionsAttribute.cs). From the following code we can see how to determine the accepted targets of an attribute (AttributeTargets), if multiple instances can be used (AllowMultiple), and if it can be inherited (Inherited ).

using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace CustomFilter
{
    /// <summary>
    /// A filter attribute that exposes the <see cref="MyMultipleActionsFilter"/> implementation.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public class MyMultipleActionsAttribute : Attribute, IFilterFactory
    {
        public bool IsReusable => false;

        public bool Enabled { get; set; } = true;

        public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
        {
            var loggerFactory = (ILoggerFactory)serviceProvider.GetService(typeof(ILoggerFactory));

            MyMultipleActionsFilter multipleFilter = new MyMultipleActionsFilter(Enabled, loggerFactory);
            return multipleFilter;
        }
    }
}

The MyMultipleActionsFilter implements the following interfaces IResourceFilter, IActionFilter, IExceptionFilter, and IResultFilter. The following code shows a part of the complete implementation to present the main logic.

public class MyMultipleActionsFilter : IResourceFilter, IActionFilter, IExceptionFilter, IResultFilter
{
    private readonly bool _enabled;
    private readonly ILogger _logger;

    public MyMultipleActionsFilter(bool enabled, ILoggerFactory loggerFactory)
    {
        // ...
    }

    /// <summary>
    /// IResourceFilter.OnResourceExecuting: OnResourceExecuting: Run before the rest of the filter pipeline.
    /// </summary>
    /// <param name="context"></param>
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        if (!_enabled) return;
        _logger.LogInformation("Run: [Before] IResourceFilter.OnResourceExecuting.");
    }

    /// <summary>
    /// IActionFilter.OnActionExecuting: Called before the action execution and after the model binding is complete.
    /// </summary>
    /// <param name="context"></param>
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // ...
    }

    /// <summary>
    /// An IExceptionFilter implementation that differentiates the exception handles depending on the Accept header.
    /// </summary>
    /// <param name="context"></param>
    public void OnException(ExceptionContext context)
    {
        if (!_enabled)
            return;

        // In this example, we only handle the `NullReferenceException`.
        if (!context.ExceptionHandled && context.Exception is NullReferenceException)
        {
            // In case that the client requests a JSON response (e.g. an API-client).
            if (context.HttpContext.Request.Headers[HeaderNames.Accept].Any(h => h == "application/json"))
            {
                context.Result = new ObjectResult(new
                {
                    StatusCode = 500,
                    Error = context.Exception.Message,
                    ErrorCode = "NullReference"
                });
            }
            // In other cases, we assume that the client is a Website, so we could redirect the user to an error page.
            else
            {
                context.Result = new RedirectResult("/myErrorPage");
            }

            context.ExceptionHandled = true;
        }
    }
}

Apply Filters and Examine Results

The filter attributes that we have created can be added to the filter pipeline by instance. This means that we can use the created attributes with their names (without the Attribute suffix), e.g. the AddHeaderAttribute can be used as [AddHeader] and MyMultipleActionsAttribute as [MyMultipleActions].

As we can see in the following code example, we have applied the [AddHeader] attribute in two cases:

  • At the WeatherForecastController with the headergeneral and value1 parameters. This attribute will be applied to all action methods of the controller and will return an HTTP-header with name = headergeneral and value = value1, as we can see in Figure 2.
  • At the GetById action method with the headerspecific and new string[] { "value2", "value3" }parameters. This attribute will be applied only to the current action method, and will return an HTTP-header with name = headerspecific with two values “value2”and “value3”, as we can see in Figure 3.

The [MyMultipleActions] attribute has been applied separately to the actions methods with different input parameters. Based on the Enabled parameter’s value, the attribute will be active (by adding logs and catching exceptions) or not.

  • At the GetAll action method with the Enabled = false parameter. As we can see in Figure 2, there are no logs regarding the MyMultipleActions filter.
  • At the GetById action method with the Enabled = true parameter.
    • From Figure 3, we can see that the MyMultipleActions filter has added a log for each stage of the filter pipeline, as expected based on Figure 1.
    • Figure 4 shows how to call the GetById action by using a browser (the Accept HTTP-header is set automatically to text/html). As we can see, the page redirects to an error page.
    • Figure 5 shows how to call the GetById action by using the Postman application by setting the Accept HTTP-header to return a JSON response. Also, we can see the actual JSON response.
namespace ExampleWebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    [AddHeader("headergeneral", "value1")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[] {...};
        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger) {...}

        [HttpGet]
        [MyMultipleActions(Enabled = false)]
        public IEnumerable<WeatherForecast> GetAll()
        {
            _logger.LogInformation("Run Action Method: WeatherForecastController.GetAll()");

            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast {...})
            .ToArray();
        }

        [HttpGet("{Id}")]
        [AddHeader("headerspecific", new string[] { "value2", "value3" })]
        [MyMultipleActions(Enabled = true)]
        public WeatherForecast GetById(int id)
        {
            _logger.LogInformation("Run Action Method: WeatherForecastController.GetById()");

            if (id == 5)
            {
                throw new NullReferenceException($"This is a test NullReferenceException for value {id}");
            }

            var rng = new Random();
            return new WeatherForecast {...};
        }
    }
}
The response of the GetAll() action method by using the [AddHeader] attribute.
Figure 2. - The response of the GetAll() action method by using the [AddHeader] attribute.
The response of the GetById () action method by using the [MyMultipleActions] attribute.
Figure 3. - The response of the GetById() action method by using the [MyMultipleActions] attribute.
Redirect on an exception when the Accept header is not used.
Figure 4. - Redirect on an exception when the Accept header is not used.
Return a JSON response on an exception when the Accept header is set to 'application/json'.
Figure 5. - Return a JSON response on an exception when the Accept header is set to "application/json".

Testing a Filter

When applying unit testing, we are breaking down the code functionalities into testable scenarios-behaviours to test them as individual units. Unit testing can greatly affect the quality of the code if it is used as part of the development flow (e.g. defined in the definition of done). It is recommended to create unit tests that verify the behaviour of the code as soon as it’s written. But, this is an investment that has to be agreed with the team and company.

The following scenarios-behaviours of our filter attributes will be verified by creating unit tests. But, these scenarios are not complete. In a real-life application, many more unit tests would be created to cover more cases.

  • The AddHeaderAttribute is expected to add a header (with predefined name and value(s)) to the HTTP response.
  • The MyMultipleActionsFilter is expected to add an information log each time a different filter type event is called.

As we can see in the following unit tests, we are using the AAA (Arrange, Act, and Assert) pattern to separate the relative sections of code. For the Arrange section, we are initializing the following objects depending on our case-scenario to imitate the actual conditions in which the filter will be executed.

  • The custom input parameters depending on the logic of the current filter.
  • An ActionContext object, depending on our case-scenario to imitate the actual conditions in which the filter will be executed.
  • The context for the specific filter type and method, depending on our case-scenario.
public class AddHeaderAttribute_Tests
{
    [Fact]
    public void OnResultExecuting_ShoultAddTheExpectedHeaderAtTheResponse()
    {
        // Arrange (Initialize the necessary objects)
        string headersName = "aheadername";
        string headersValue = "a test header value";

        // Create a default ActionContext (depending on our case-scenario)
        var actionContext = new ActionContext()
        {
            HttpContext = new DefaultHttpContext(),
            RouteData = new RouteData(),
            ActionDescriptor = new ActionDescriptor()
        };

        // Create the filter input parameters (depending on our case-scenario)
        var resultExecutingContext = new ResultExecutingContext(
            actionContext,
                new List<IFilterMetadata>(),
                new ObjectResult("A dummy result from the action method."),
                Mock.Of<Controller>()
            );

        // Act (Call the method under test with the arranged parameters)
        AddHeaderAttribute addHeaderAttribute = new AddHeaderAttribute(headersName, headersValue);
        addHeaderAttribute.OnResultExecuting(resultExecutingContext);

        // Assert (Verify that the action of the method under test behaves as expected)
        Assert.Equal(1, resultExecutingContext.HttpContext.Response.Headers.Count);
        Assert.True(resultExecutingContext.HttpContext.Response.Headers.ContainsKey(headersName));
        Assert.Equal(headersValue, resultExecutingContext.HttpContext.Response.Headers[headersName]);
    }
}
public class MyMultipleActionsFilter_Tests
{
    [Fact]
    public void OnResourceExecuting_ShoultAddASingleLogIfEnabled()
    {
        // Arrange (Initialize the necessary objects)
        bool isFilterEnabled = true;
        var loggerMock = new Mock<ILogger<MyMultipleActionsFilter>>();

        // Create a default ActionContext (depending on our case-scenario)
        var actionContext = new ActionContext()
        {
            HttpContext = new DefaultHttpContext(),
            RouteData = new RouteData(),
            ActionDescriptor = new ActionDescriptor()
        };

        // Create the filter input parameters (depending on our case-scenario)
        var resourceExecutingContext = new ResourceExecutingContext(
            actionContext,
            new List<IFilterMetadata>(),
            new List<IValueProviderFactory>()
            );


        // Act (Call the method under test with the arranged parameters)
        MyMultipleActionsFilter multipleActionsFilter = new MyMultipleActionsFilter(isFilterEnabled, loggerMock.Object);
        multipleActionsFilter.OnResourceExecuting(resourceExecutingContext);

        // Assert (Verify that the action of the method under test behaves as expected)
        loggerMock.Verify(
            m => m.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<FormattedLogValues>(v => v.ToString().Contains("IResourceFilter.OnResourceExecuting")),
                It.IsAny<Exception>(),
                It.IsAny<Func<object, Exception, string>>()
            )
        );
    }
}

Summary

A filter in ASP.NET Core can be used to inject code in the filter-pipeline. Also, filters can be used to extract repetitive code out of the action methods. In this article, we focused on creating and testing filter attributes. Before the implementation of a filter, we can start by making some decisions based on the current goal. This will help us select the proper filter type (or types), methods, and Interfaces or base class that should be implemented.

We have created two filter attributes based on our needs-goals. Our created filters (ResultFilterAttribute, IFilterFactory, IExceptionFilter, etc.) were applied in a Web API project in which we verified their results for different input parameters. For example, our implementation of the IExceptionFilter differentiates the exception handling depending on the value of the Accept header. Finally, we saw how to create unit tests for specific scenarios-behaviours of our created filters.

Filters are a great way to inject code in the filter-pipeline and to extract repetitive code out of the action methods. I hope that you are already thinking about the cases in which filters can be used in your projects 🙂.

If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!

Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.