Understanding The Filter Pipeline In ASP.NET Core

Hey guys Iโ€™m so happy to be writing again after a long absence I was sick and also Iโ€™m looking for a job so a lot of things are happening right now but the post I have prepared for you today is all worth the wait.

In my last article, I talked to you guys about how to use the patch request to partially update your web API resources and I put a lot of effort to show you all the types of operations that the patch method supports and how you could use them so I leave a link to that article if you want to check it out. Ok now let's get into today's article.

Actually, when I first learned about action filters I had a hard time understanding them and they did not look appealing or exciting to me so I pushed myself to my limit to bring you something amazing even though I had a hard time enjoying it.

As a developer, you must have heard of the term 'Action Filters' when working with ASP.NET Core. Action filters are an essential part of the framework and can help you add additional functionality to your application. In this blog, we will explore what action filters are and why you should use them.

What is an Action Filter โ“

An action filter is a type of filter that allows you to add additional behavior to the MVC framework's action methods. You can use them to execute code before or after an action method is executed, or to modify the results returned by the action method.

Why should I use Action Filters โ‰๏ธ

Action filters offer several benefits, including:

  1. Code Reusability: Action filters are reusable components that can be applied to multiple action methods, reducing code duplication and increasing maintainability.

  2. Separation of Concerns: Action filters allow you to separate cross-cutting concerns from the rest of your code. For example, you can use an action filter to handle logging or caching logic, leaving your action methods focused on their specific functionality.

  3. Customization: You can create custom action filters to add functionality that is specific to your application's needs.

Code Example of an Action Filter ๐Ÿง‘๐Ÿพโ€๐Ÿ’ป

Let's take a look at an example of an action filter that logs the time it takes to execute an action method.

public class LogActionFilter : IActionFilter
{
    private readonly ILogger _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation($"Executing action {context.ActionDescriptor.DisplayName}.");
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation($"Executed action {context.ActionDescriptor.DisplayName} in {context.HttpContext.Response.Headers["X-Elapsed-Time"]}.");
    }
}

In this example, we created a custom action filter called LogActionFilter that logs the time it takes to execute an action method. The OnActionExecuting method is called before the action method is executed, and the OnActionExecuted method is called after the action method is executed.

To use this action filter, we need to register it in the ConfigureServices method of our startup class:

services.AddScoped<LogActionFilter>();

And then apply it to an action method by adding the [ServiceFilter(typeof(LogActionFilter))] attribute to the action method:

[ServiceFilter(typeof(LogActionFilter))]
public IActionResult Index()
{
    // Do something here
}

Type of Filters ๐ŸŒ€

ASP.NET Core has six types of filters: Authorization filters, Resource filters, Action filters, and Exception filters. Let's take a closer look at each type along with a code example for each.

1. Authorization Filters ๐Ÿ‘ฎ๐Ÿพ

Authorization filters are used to check if a user is authorized to access a particular resource or page. They are executed before the action method is invoked.

Here's an example of an authorization filter that checks if the user is authenticated:

public class CustomAuthorizationFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            // Redirect to login page or return unauthorized response
            context.Result = new RedirectToActionResult("Login", "Account", null);
        }
    }
}

To apply this authorization filter to an action method, you can use the [Authorize] attribute along with the filter:

[Authorize]
[TypeFilter(typeof(CustomAuthorizationFilter))]
public IActionResult SecureAction()
{
    // Code for secure action
}

2. Resource Filters โ›‘๏ธ

Resource filters are used to perform tasks that are related to a particular resource. They are executed before and after the action method.

Here's an example of a resource filter that logs the start and end of the action method execution:

public class LoggingResourceFilter : IResourceFilter
{
    private readonly ILogger _logger;

    public LoggingResourceFilter(ILogger<LoggingResourceFilter> logger)
    {
        _logger = logger;
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        _logger.LogInformation("Executing resource filter - Before action method");
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        _logger.LogInformation("Executing resource filter - After action method");
    }
}

To apply this resource filter to an action method, you can use the [TypeFilter] attribute:

[TypeFilter(typeof(LoggingResourceFilter))]
public IActionResult MyAction()
{
    // Code for the action method
}

3. Action Filters ๐ŸŽฌ

Action filters are used to perform tasks that are related to a particular action method. They are executed before and after the action method.

Here's an example of an action filter that logs the time it takes to execute an action method:

public class TimingActionFilter : IActionFilter
{
    private readonly Stopwatch _stopwatch;

    public TimingActionFilter()
    {
        _stopwatch = new Stopwatch();
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _stopwatch.Start();
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _stopwatch.Stop();
        var elapsedMilliseconds = _stopwatch.ElapsedMilliseconds;
        // Log or use the elapsed time as needed
    }
}

To apply this action filter to an action method, you can use the [ServiceFilter] attribute:

[ServiceFilter(typeof(TimingActionFilter))]
public IActionResult MyAction()
{
    // Code for the action method
}

4. Exception Filters ๐Ÿšซ

Exception filters are used to handle exceptions that occur during the execution of an action method. They are executed when an exception is thrown.

Here's an example of an exception filter that handles and logs exceptions:

public class ExceptionLoggingFilter : IExceptionFilter
{
    private readonly ILogger _logger;

    public ExceptionLoggingFilter(ILogger<ExceptionLoggingFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Exception occurred");
        // Handle the exception or perform any necessary actions
    }
}

To apply this exception filter to an action method, you can use the [TypeFilter] attribute:

[TypeFilter(typeof(ExceptionLoggingFilter))]
public IActionResult MyAction()
{
    // Code for the action method
}

5. Endpoint Filters ๐Ÿ”š

Endpoint filters are used to perform actions before and after an endpoint is selected during the routing process. They can be used to modify the selected endpoint or add additional behavior.

Here's an example of an endpoint filter that modifies the endpoint's metadata:

public class CustomEndpointFilter : IEndpointFilter
{
    public void OnEndpointSelected(EndpointSelectedContext context)
    {
        // Modify endpoint metadata or perform additional actions
        var endpoint = context.Endpoint;
        // Modify the endpoint as needed
    }
}

To apply this endpoint filter, you can use the AddEndpointFilter extension method in the ConfigureServices method of your Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddEndpointFilter<CustomEndpointFilter>();
}

6. Result Filters ๐Ÿ’ฏ

Result filters are used to perform actions before and after the execution of an action result. They can modify the result, add headers, or perform additional operations.

Here's an example of a result filter that modifies the result by adding a custom header:

public class CustomResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        // Modify the result or add headers before the execution
        context.HttpContext.Response.Headers.Add("CustomHeader", "CustomValue");
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Perform additional actions after the execution
    }
}

To apply this result filter to an action method, you can use the [ServiceFilter] attribute:

[ServiceFilter(typeof(CustomResultFilter))]
public IActionResult MyAction()
{
    // Code for the action method
}

Alternatively, you can apply the result filter globally to all action methods by using the AddMvcOptions method in the ConfigureServices method of your Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.Filters.Add<CustomResultFilter>();
    });
}

By utilizing endpoint filters, you can modify endpoint metadata and add behavior during the routing process. Result filters, on the other hand, allow you to modify action results and perform additional actions before and after their execution. These filters provide flexibility and control over the behavior of your ASP.NET Core application.

By utilizing these different types of filters, you can enhance the behavior and functionality of your ASP.NET Core application, adding security checks, logging, exception handling, and resource-related tasks.

Filters Scope ๐Ÿ”ฌ

Filters can be applied at different scopes, depending on where you want them to take effect. Let's explore the three main scopes: Global, Controller, and Action.

Global Filters ๐ŸŒ

Global filters are applied to all action methods in your application. They are registered during application startup and provide common functionality or handle cross-cutting concerns throughout your application.

Example:

// Custom global filter implementing IActionFilter
public class GlobalLoggingFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Perform actions before the action method execution
        // Example: Logging or request validation
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Perform actions after the action method execution
        // Example: Logging or response modification
    }
}

// Registering global filter in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.Filters.Add<GlobalLoggingFilter>();
    });
}

Controller Filters ๐ŸŽฎ

Controller filters are applied to all action methods within a specific controller. They allow you to apply behavior that is specific to a group of related actions.

Example:

// Custom controller filter implementing IActionFilter
public class AuthenticationFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Perform actions before the action method execution
        // Example: Authentication or authorization checks
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Perform actions after the action method execution
    }
}

// Applying controller filter to a controller
[TypeFilter(typeof(AuthenticationFilter))]
public class MyController : Controller
{
    // Actions within this controller will be subject to the AuthenticationFilter
}

Action Filters ๐Ÿคฉ

Action filters are applied to individual action methods, allowing you to customize the behavior of specific actions.

Example:

// Custom action filter implementing IActionFilter
public class LoggingFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Perform actions before the action method execution
        // Example: Logging or input validation
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Perform actions after the action method execution
    }
}

// Applying an action filter to an action method
[ServiceFilter(typeof(LoggingFilter))]
public IActionResult MyAction()
{
    // Code for the action method
}

These examples demonstrate the usage of action filters at different scopes. Global filters provide common functionality for all actions, controller filters apply behavior to a specific group of actions within a controller, and action filters allow fine-grained control over individual action methods. By utilizing these action filter scopes, you can add the desired behavior and customization to your ASP.NET Core application.

By using these different scopes, you can apply action filters where they are most needed. Global filters provide overarching functionality across your entire application, controller filters offer behavior specific to a group of related actions, and action filters allow you to fine-tune the behavior of individual action methods.

Remember, each scope serves a unique purpose, and you can mix and match them based on your application's requirements. Whether you need broad protection, controller-specific enhancements, or action-level customization, action filter scopes in ASP.NET Core have got you covered!

Implementing your Own Action Filter ๐Ÿ—ƒ๏ธ

Even thou we reviewed all filter types that ASP NET Core supports we will focus on implementing an action filter to our web API that validates data. The action filter to implement will validate if the Post object is null as this is a repetitive snippet of code that needs to be added to various endpoints in the PostsController.

Clone the Repository Code on GitHub ๐Ÿ™

Remember that if you want to follow along with the tutorial get the latest code changes up to this article from the Web API Repository at Github on the Patch branch

Create a new Action Controller Class ๐ŸŽฌ

Letโ€™s begin by first creating a new action controller that will validate if the Post we want to Get, Delete, Edit, Patch actually exists and this way we will avoid duplicating that code in our controller actions.

public class PostExistsFilter : IAsyncActionFilter
{
    private readonly IPostRepository postRepository;

    public PostExistsFilter(IPostRepository postRepository)
    {
        this.postRepository = postRepository;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        if (!context.ActionArguments.ContainsKey("id"))
        {
            context.Result = new BadRequestResult();
            return;
        }

        if (!(context.ActionArguments["id"] is int id))
        {
            context.Result = new BadRequestResult();
            return;
        }

        var post = await postRepository.GetPostAsync(id);

        if (post is null)
        {
            context.Result = new NotFoundObjectResult($"Post with id {id} does not exist.");
            return;
        }

        await next();
    }
}

You will create a new class called PostExistsFilter that implements the IAsyncActionFilter and this will be your ActionFilter, you have to use the async version as the IPostRepository uses the GetPostAsync function.

The IPostRepository interface will be injected using constructor injection.

Next, you will have to implement the interface method OnActionExutionAsync which provides the ActionExecutionContext and ActionExecutionDelegate as parameters.

Inside the method, the method will check if the action contains an id as an argument, and if not it will throw a new BadRequestResult then in the next if statement it will check the id to be an int and again if itโ€™s not it will return a BadRequestResult.

After both if statements now that the method validated the id to exist as a parameter in the action and that the id is an int it will attempt to retrieve the post using the GetPostAsync(id) method. Last but not least if the post is null it will return a NotFoundObjectResult response with a custom error message and then call the ActionExecutionDelegate to continue with the action request.

Create an Attribute Class ๐Ÿงฒ

If you use the PostExistsFilter as it is you would need to register it as a service in the Program class and use it above actions like this:

[ServiceFilter(typeof(PostExistsFilter))]
public async Task<IActionResult> EditPost

This is not bad but we can achieve a much more clean and enhance the usability of filter attributes that depends on other classes.

So you will have to create a new attribute class called PostExistsAttribute that implements the TypeFilterAttribute base class and call the base constructor by referring to the PostExistsFilter so in much simpler words you will need to have something like the following:

public class PostExistsAttribute : TypeFilterAttribute
{
    public PostExistsAttribute() : base(typeof(PostExistsFilter))
    {
    }
}

Yeah, it is that simple. Just an attribute class that calls the base constructor passing our action filter.

This may seem odd but itโ€™s pretty useful as you will no longer need to register your Filter in the service container in the Program class and the attribute will end up looking a lot cleaner when used on the corresponding action endpoints and you will look at that in a brief.

Applying your Action Filter in the PostsController ๐ŸŽ

Now all that is left is to do is to apply the action filter to the endpoints that actually make use of the null checking for the Post entity.

[HttpGet("{id:int}")]
[PostExists]
public async Task<IActionResult> GetPost(int id)

[HttpPut("{id:int}")]
[PostExists]
public async Task<IActionResult> EditPost(int id, [FromBody] EditPostDTO editPostDto)

[HttpDelete("{id:int}")]
[PostExists]
public async Task<IActionResult> DeletePost(int id)

[HttpPatch("{id:int}")]
[PostExists]
public async Task<ActionResult> PatchPost(int id, [FromBody] JsonPatchDocument<Post> doc)

You should apply the action filter to these four endpoints and remove all code checking if the post is null. Also, you can note that the way you call the action filter is using the [PostExists] attribute instead of the old way of doing it which looked a lot harder to read.

Update to the GetPostAsync(id) method in the PostRepository ๐Ÿ—ž๏ธ

To avoid concurrency problems you will need to update the GetPostAsync(id) method that retrieves a single post.

public async Task<Post> GetPostAsync(int id)
{
    var post = await context.Posts.AsNoTracking().SingleOrDefaultAsync(x => x.Id == id);
    return post;
}

This is to avoid tracking the retrieved post as the same post will be accessed to be updated, patched, or deleted and can have concurrency problems as they will all try to access the same entity at the same time.

Take your app for a test ๐Ÿงช

Now you are ready to test your app and you will see no change if your endpoint supported the null check but if not you will notice that if the post you try to access is null then you will receive a NotFound HTTP response code.

Conclusion ๐ŸŒ‡

Action filters are an important part of the ASP.NET Core framework, and they offer several benefits, including code reusability, separation of concerns, and customization. By using action filters, you can add additional functionality to your application and make it more maintainable.

Great Announcement ๐Ÿฅณ

Iโ€™m back on YouTube so make sure you go and check out the new videos that I released these days and donโ€™t forget to subscribe, like, and share with your friends that are also learning. Also, remember that you can follow me on Twitter as Oscar Montenegro and check out my Blog Unit Coding for more awesome content about designing and developing REST Services using ASP NET 7+ and amazing web applications with Blazor. Thanks for your constant support as Iโ€™m here because of all the amazing people that follow my work, for you guys cheers ๐Ÿป. See you all in the next article!

Did you find this article valuable?

Support Unit Coding by becoming a sponsor. Any amount is appreciated!

ย