top of page

Filters in ASP.NET: How to use dependency injection in action filters in ASP.NET Core?

Updated: Jun 28, 2023


Filters are special components in ASP.NET Core that allow us to control the execution of a request at specific stages of the request pipeline. These filters come into play after the middleware execution and when the MVC middleware matches a route and invokes a specific action.


In simpler terms, filters provide a way to customize the behavior of our application at the action level. They are like checkpoints that allow us to perform specific tasks such as exception handling, caching, or adding custom response headers.


When a request reaches an action, filters can gather information about which action has been selected and the associated route data. This information can be used to make decisions and perform actions specific to that particular action or controller.


By using filters, we have more control and flexibility in handling requests. We can implement custom logic and apply specific behaviors to different actions or controllers based on our requirements. This helps in achieving a more tailored and efficient request handling process in our ASP.NET Core applications.

Types of Filters

ASP.NET Core provides a set of predefined filter types that allow us to control the execution of requests within the context of an action or controller. These filters serve different purposes and can be customized based on our requirements.


Here are the main types of predefined filters in ASP.NET Core:

  1. AuthorizationFilter: This filter is executed at the beginning of the route execution. It determines whether the user is allowed to access the route or perform the requested action.

  2. ResourceFilter: This filter runs after authorization and can be used to bypass the execution of the remaining pipeline. It is useful in scenarios where we want to send a cached response if a preprocessed response is available, thus skipping the rest of the pipeline.

  3. ActionFilter: The action filter runs before and after the execution of an action. It provides a way to surround an action with custom logic or perform additional tasks before and after the action is executed.

  4. ResultFilter: This filter is executed before and after a result is generated from an action. It allows us to surround the result execution with additional behavior or perform certain actions based on the result.

  5. ExceptionFilter: The exception filter runs whenever an uncaught exception is thrown within the action or controller. It provides a way to handle exceptions and design customized error responses specific to actions or controllers.

  6. ServiceFilter: This filter runs another filter whose type is passed within itself. It is used when the passed filter type is registered as a service. It resolves any dependencies passed to the filter type through dependency injection (DI).

  7. TypeFilter: Similar to the service filter, the type filter also runs a filter type that is not registered as a service. It allows us to apply custom filters without the need for registration.

These predefined filter types give us the flexibility to control the request execution process and add specific behavior at different stages of the pipeline in ASP.NET Core applications.

 

Scope of Filters

The scope of filters in ASP.NET Core is determined by how they are attached to the MVC pipeline. This allows us to control when and where the filters will be executed. There are three main scopes for filters:

  1. Action-specific scope: Filters can be applied to a specific action method within a controller. By decorating the action with the filter attribute, the filter will only execute when that particular action is selected to handle an incoming request.

  2. Controller-specific scope: Filters can also be applied to a whole controller class. When a filter is applied at the controller level, it will execute for all the actions within that controller.

  3. Global scope: Global filters are applied to every route that is matched and picked up by the routing middleware. These filters are registered as global filters and will be executed regardless of the specific action or controller. However, if no endpoint is selected for the request, no filter will execute.

To register a filter as a global filter, it can be added to the filters array inside the ConfigureServices() method within the Startup class. Here's an example:

services.AddControllers(options => {
    options.Filters.Add(typeof(ConsoleGlobalActionFilter));
});

In this example, the ConsoleGlobalActionFilter is registered as a global filter using the Add(typeof(...)) method. This ensures that the filter will be applied to every route that is matched and processed by the routing middleware.


By controlling the scope of filters, we can determine precisely when and where the filters will be applied in our ASP.NET Core application, allowing us to add specific behavior and control the request execution process effectively.

 

How to Create a Simple Filter

There are two ways to create filters of the types we discussed earlier. Let's take the example of creating an ActionFilter:

  1. Implementing the interface: To create a custom action filter that acts upon action execution, we can create a class that implements the IActionFilter or IAsyncActionFilter interfaces. These interfaces provide methods that we can override to add our custom logic. By implementing these interfaces, we have full control over the behavior of the action filter.

  2. Extending the attribute class: Alternatively, we can create a custom action filter class that extends the ActionFilterAttribute class. The ActionFilterAttribute class already implements the IActionFilter and IAsyncActionFilter interfaces. By extending this class, we can override the methods that we're interested in and add our custom logic. This approach provides a more convenient way to create action filters as we can directly extend the base class and focus on implementing the necessary methods.

Similarly, for the other types of filters mentioned (such as authorization filter, resource filter, result filter, and exception filter), there are corresponding interfaces that can be implemented or attribute classes that can be extended.


Here are the interfaces and attribute classes for the mentioned filters:

  • IAuthorizationFilter: Can be implemented or extended by AuthorizationFilterAttribute.

  • IResourceFilter: Can be implemented or extended by ResourceFilterAttribute.

  • IActionFilter: Can be implemented or extended by ActionFilterAttribute.

  • IResultFilter: Can be implemented or extended by ResultFilterAttribute.

  • IExceptionFilter: Can be implemented or extended by ExceptionFilterAttribute.

Some of these filters are accompanied by attribute classes that implement the corresponding interfaces and give us the flexibility to override the specific methods that suit our needs. Here are the attribute classes for some of the filters:

  • ActionFilterAttribute: This attribute class implements the IActionFilter and IAsyncActionFilter interfaces. By extending this class, we can directly inherit the implementation of these interfaces and override the methods that we are interested in. This allows us to customize the behavior of the action filter according to our requirements.

  • ExceptionFilterAttribute: This attribute class implements the IExceptionFilter interface. By extending this class and overriding its methods, we can handle and customize the handling of exceptions that occur within the action or controller. This gives us control over how exceptions are handled and allows us to provide custom error responses.

  • ResultFilterAttribute: This attribute class implements the IResultFilter interface. By extending this class and overriding its methods, we can modify the result generated from an action before it is sent back to the client. This allows us to add additional processing or transformations to the result.

  • FormatFilterAttribute: This attribute class is used to specify the supported response formats for an action or controller. It helps in content negotiation and selecting the appropriate response format based on the client's preferences.

  • ServiceFilterAttribute: This attribute class is used to apply a filter that is registered as a service. It allows us to resolve any dependencies required by the filter using dependency injection. It is helpful when the filter requires additional services or dependencies to function correctly.

  • TypeFilterAttribute: This attribute class is similar to the ServiceFilterAttribute, but it allows us to apply a filter that is not registered as a service. We can directly specify the filter type and its dependencies.

For example, the ActionFilterAttribute class already implements the IActionFilter and IAsyncActionFilter interfaces for us, so we can extend the ActionFilterAttribute class directly and override the methods we’re interested in.

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

public class AddResponseHeaderFilter : ActionFilterAttribute 
{     
    // Async method which can surround the action execution
    // Invoked before and after the action execution
    public asyn coverride Task OnActionExecutionAsync(         ActionExecutingContext context, ActionExecutionDelegate next)     
    {         
        // Access the request         
        context.HttpContext.Response.Headers.Add(             
            "X-MyCustomHeader", Guid.NewGuid().ToString());                  
        
        var result = await next.Invoke();                  
        
        // Access the response         
        Console.WriteLine(JsonConvert.SerializeObject(result.Result));     
    } 
}  

[Route("api/[controller]")] 
[ApiController] 
public class HomeController : ControllerBase 
{     
    [AddResponseHeaderFilter]     
    [Route("")]     
    [HttpGet]     
    public IActionResult Index()     
    {         
        return Ok(new { Message = "I'm Alive" });     
    } 
}

In this example, we have created a custom AddResponseHeaderFilter class by extending the ActionFilterAttribute. This filter adds a custom response header to the HTTP response before and after the execution of the action. It overrides the OnActionExecutionAsync method, which allows us to customize the behavior surrounding the action execution.

We then apply the AddResponseHeaderFilter filter to the Index action of the HomeController class using the [AddResponseHeaderFilter] attribute. This ensures that the filter is executed for that specific action.

When the action is invoked, the filter adds a custom header to the response using the Response.Headers.Add method. It then calls the next.Invoke() method to proceed with the action execution. After the action has executed, it accesses the response result and writes it to the console using JsonConvert.SerializeObject.

By utilizing the ActionFilterAttribute and creating custom filter classes, we can extend and customize the behavior of the ASP.NET Core request pipeline to add additional functionality and modify the response based on our requirements.

 

Action Filters Implementation

Example 1: Synchronous Action Filter implementing IActionFilter

using Microsoft.AspNetCore.Mvc.Filters;

namespace ActionFilters.Filters
{
    public class ActionFilterExample : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // Code executed before the action method executes
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // Code executed after the action method executes
        }
    }
}

In this example, we create a class named ActionFilterExample that implements the IActionFilter interface. This interface requires us to implement two methods: OnActionExecuting and OnActionExecuted. The OnActionExecuting method contains the code that will be executed before the action method, while the OnActionExecuted method contains the code that will be executed after the action method.


Example 2: Asynchronous Action Filter implementing IAsyncActionFilter

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

namespace ActionFilters.Filters
{
    public class AsyncActionFilterExample : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // Code executed before the action method executesvar result = await next();

            // Code executed after the action method executes
        }
    }
}

In this example, we create a class named AsyncActionFilterExample that implements the IAsyncActionFilter interface. This interface requires us to implement the OnActionExecutionAsync method, which is an asynchronous version of the action filter. The method receives an ActionExecutingContext and an ActionExecutionDelegate. The ActionExecutionDelegate represents the next action in the pipeline, and we can await it to execute the action method. By doing so, we can run code before and after the action method execution.


By creating classes that implement either IActionFilter or IAsyncActionFilter, we can define custom logic to be executed before and after the action method in ASP.NET Core. These action filters provide a way to add additional behavior to the request pipeline and modify the request or response as needed.


The Scope of Action Filters

Action filters can be added at different scope levels: Global, Action, and Controller.


1. Global Scope

To use an action filter globally, you can register it inside the AddControllers() method in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(config =>
    {
        config.Filters.Add(new GlobalFilterExample());
    });
}

In .NET 6 and above, with the absence of the Startup class, you can use the Program class:

builder.Services.AddControllers(config =>
{
    config.Filters.Add(new GlobalFilterExample());
});

By adding the filter to the global scope, it will be applied to all action methods throughout the application.


2. Action and Controller Scope

If you want to use an action filter at the action or controller level, you need to register it as a service in the IoC container within the ConfigureServices method:

services.AddScoped<ActionFilterExample>();
services.AddScoped<ControllerFilterExample>();

In .NET 6 and above:

builder.Services.AddScoped<ActionFilterExample>();
builder.Services.AddScoped<ControllerFilterExample>();

By registering the action filter as a service, you can apply it selectively to specific action methods or controllers by decorating them with the respective filter attribute.


Finally, to use a filter registered at the action or controller level, you need to apply it on top of the respective controller or action method using the ServiceFilter attribute:

namespace AspNetCore.Controllers
{
    [ServiceFilter(typeof(ControllerFilterExample))]
    [Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        [HttpGet]
        [ServiceFilter(typeof(ActionFilterExample))]
        public IEnumerable<string> Get()
        {
            return new string[] { "example", "data" };
        }

    }
}

By using the [ServiceFilter] attribute and specifying the filter type, you can associate the registered filters (ControllerFilterExample and ActionFilterExample) with the controller and action method respectively.


This allows the filters to be applied to the designated scopes, controlling the execution flow and providing additional functionality before and after the action method is executed.


Using the ServiceFilter attribute in this way helps you leverage the benefits of dependency injection and apply the desired filters to specific controllers and action methods within your ASP.NET application.


Order of Invocation

The order in which our filters are executed is as follows:


We can change the order of invocation for multiple filters by adding an additional property called Order to the ServiceFilter attribute:

namespace AspNetCore.Controllers
{
    [ServiceFilter(typeof(ControllerFilterExample), Order=2)]
    [Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        [HttpGet]
        [ServiceFilter(typeof(ActionFilterExample), Order=1)]
        public IEnumerable<string> Get()
        {
            return new string[] { "example", "data" };
        }

    }
}

In this example, the ControllerFilterExample will be executed before the ActionFilterExample due to their respective order values of 2 and 1. By assigning different order values to the filters, you can control the sequence in which they are executed.


You can also apply multiple filters to the same action method and define their order of execution:

[HttpGet]
[ServiceFilter(typeof(ActionFilterExample), Order=2)]
[ServiceFilter(typeof(ActionFilterExample2), Order=1)]
public IEnumerable<string> Get()
{
    return new string[] { "example", "data" };
}

In this case, ActionFilterExample2 will be executed before ActionFilterExample based on their order values of 1 and 2 respectively.

By specifying the order for each filter, you have fine-grained control over the execution sequence and can apply multiple filters with different priorities to achieve the desired behavior in your ASP.NET application.

Improving the Code with Action Filters

To improve the code with action filters, let's focus on the MoveController class in the Controllers folder of the starting project from the AppStart folder in our repository. This controller contains implementations for all the CRUD operations.


Although our actions are already clean and readable, thanks to global exception handling, we can further enhance them.


One important thing to note is that our Movie model inherits from the IEntity interface::

[Table("Movie")]
public class Movie: IEntity
{
    [Key]
    public Guid Id { get; set; }
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }
    [Required(ErrorMessage = "Genre is required")]
    public string Genre { get; set; }
    [Required(ErrorMessage = "Director is required")]
    public string Director { get; set; }
}

Now, let's focus on the validation code for the POST and PUT actions. By applying the appropriate action filter to the POST and PUT actions, we can eliminate the need for explicit validation code in those actions, making them more concise and maintainable.


Validation with Action Filters

To improve the validation code in our POST and PUT actions, we can use action filters. By extracting the validation logic into a custom action filter, we can make the code more reusable and keep the actions cleaner.


Let's start by creating a new folder named "ActionFilters" in our solution explorer. Inside that folder, create a new class called ValidationFilterAttribute:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Linq;

namespace ActionFilters.ActionFilters
{
    public class ValidationFilterAttribute : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            var param = context.ActionArguments.SingleOrDefault(p => p.Value is IEntity);
            if (param.Value == null)
            {
                context.Result = new BadRequestObjectResult("Object is null");
                return;
            }
            
            if (!context.ModelState.IsValid)
            {
                context.Result = new UnprocessableEntityObjectResult(context.ModelState);
            }
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {          
        }
    }
}

In the ValidationFilterAttribute class, we override the OnActionExecuting method to perform our validation logic. We check if the action argument is of type IEntity, and if it's null, we return a BadRequestObjectResult. Additionally, if the ModelState is not valid, we return an UnprocessableEntityObjectResult.


Next, let's register this action filter as a service in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MovieContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("sqlConString")));

    services.AddScoped<ValidationFilterAttribute>();

    services.AddControllers();
}

For .NET 6, we need to use the builder variable inside the Program class:

builder.Services.AddDbContext<MovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("sqlConString")));

builder.Services.AddScoped<ValidationFilterAttribute>();

builder.Services.AddControllers();

Finally, remove the validation code from our POST and PUT actions and apply the ValidationFilterAttribute as a service:

[HttpPost]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public IActionResult Post([FromBody] Movie movie)
{
    _context.Movies.Add(movie);
    _context.SaveChanges();

    return CreatedAtRoute("MovieById", new { id = movie.Id }, movie);
}

[HttpPut("{id}")]
[ServiceFilter(typeof(ValidationFilterAttribute))]
public IActionResult Put(Guid id, [FromBody] Movie movie)
{
    var dbMovie = _context.Movies.SingleOrDefault(x => x.Id.Equals(id));
    if (dbMovie == null)
    {
        return NotFound();
    }

    dbMovie.Map(movie);

    _context.Movies.Update(dbMovie);
    _context.SaveChanges();

    return NoContent();
}

By applying the ValidationFilterAttribute as a service filter, we eliminate the need for the validation code in our actions. The code is now cleaner and more readable. Additionally, this validation logic is reusable as long as our model classes inherit from the IEntity interface.


To ensure that the action filter's validation takes precedence over the default validation behavior of the [ApiController] attribute, we need to suppress the default validation. In the Startup class (or the Program class for .NET 6), add the following configuration:

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

This configuration ensures that the action filter's validation result (e.g., UnprocessableEntity) is returned instead of the default BadRequest result for validation errors.


With these changes, our validation filter is ready to be tested.


Dependency Injection in Action Filters

To eliminate the code repetition for fetching the movie by ID from the database and checking its existence in the GetById, DELETE, and PUT actions, we can create a new action filter that performs this task. We'll also use dependency injection to inject the MovieContext into the action filter.

Let's create a new action filter class called ValidateEntityExistsAttribute<T> in the ActionFilters folder:


using System; 
using System.Linq; 
using Microsoft.AspNetCore.Mvc; 
using Microsoft.AspNetCore.Mvc.Filters;  

namespace ActionFilters.ActionFilters 
{     
    public class ValidateEntityExistsAttribute<T> : IActionFilter where T : class, IEntity     
    {         
        private readonly MovieContext _context;          
    
        public ValidateEntityExistsAttribute(MovieContext context)         
        {             
            _context = context;         
        }          
        
        public void OnActionExecuting(ActionExecutingContext context)         
        {             
            Guid id = Guid.Empty;              
            
            if (context.ActionArguments.ContainsKey("id"))             
            {                 
                id = (Guid)context.ActionArguments["id"];             
            }             
            else             
            {                 
                context.Result = new BadRequestObjectResult("Bad id parameter");                 
                return;             
            }              
                
            var entity = _context.Set<T>().SingleOrDefault(x => x.Id.Equals(id));             
            if (entity == null)             
            {                 
                context.Result = new NotFoundResult();             
            }             
            else             
            {                 
                context.HttpContext.Items.Add("entity", entity);             
            }         
        }          
        public void OnActionExecuted(ActionExecutedContext context)         
        {         
        }     
     } 
} 

In the ValidateEntityExistsAttribute<T> class, we inject the MovieContext via the constructor using dependency injection. The class is made generic so that it can be reused for any model in our project. In the OnActionExecuting method, we fetch the ID parameter from the action context and check if the entity exists in the database. If the entity is found, we store it in the HttpContext.Items collection for later use in the action methods.

Next, let's register the action filter in the ConfigureServices method:

services.AddScoped<ValidateEntityExistsAttribute<Movie>>(); 

For .NET 6, we use the builder variable in the Program class:

builder.Services.AddScoped<ValidateEntityExistsAttribute<Movie>>(); 

Finally, modify our actions to apply the ValidateEntityExistsAttribute as a service filter:

[HttpGet("{id}", Name = "MovieById")] [ServiceFilter(typeof(ValidateEntityExistsAttribute<Movie>))] 
public IActionResult Get(Guid id) 
{     
    var dbMovie = HttpContext.Items["entity"] as Movie;      
    return Ok(dbMovie); 
}  

[HttpPut("{id}")] 
[ServiceFilter(typeof(ValidationFilterAttribute))] 
[ServiceFilter(typeof(ValidateEntityExistsAttribute<Movie>))] 
public IActionResult Put(Guid id, [FromBody]Movie movie) 
{     
    var dbMovie = HttpContext.Items["entity"] as Movie;      
    
    dbMovie.Map(movie);      
    
    _context.Movies.Update(dbMovie);     
    _context.SaveChanges();      
    
    return NoContent(); 
}  

[HttpDelete("{id}")] 
[ServiceFilter(typeof(ValidateEntityExistsAttribute<Movie>))] 
public IActionResult Delete(Guid id) 
{     
    var dbMovie = HttpContext.Items["entity"] as Movie;      
    
    _context.Movies.Remove(dbMovie);     
    _context.SaveChanges();      
    
    return NoContent(); 
} 

By applying the ValidateEntityExistsAttribute<Movie> as a service filter, we ensure that the movie entity is fetched from the database and made available in the action methods via HttpContext.Items. This eliminates the need for repeated code in fetching the entity and checking its existence.

With these changes, our actions are cleaner and more readable, and the code for fetching the entity by ID is now reusable.

0 comments
bottom of page