Filters in .NET offer a great way to hook into the MVC action invocation pipeline. Therefore, we can use filters to extract code that can be reused and make our actions cleaner and maintainable. There are some filters that are already provided by ASP.NET Core like the authorization filter, and there are the custom ones that we can create ourselves.
There are different filter types:
- Authorization filters – They run first to determine whether a user is authorized for the current request
- Resource filters – They run right after the authorization filters and are very useful for caching and performance
- Action filters – They run right before and after the action method execution
- Exception filters – They are used to handle exceptions before the response body is populated
- Result filters – They run before and after the execution of the action methods result.
In this article, we are going to talk about Action filters and how to use them to create cleaner and reusable code in our Web API.
VIDEO: Implementing Action Filters in ASP.NET Core Video.
Let’s start.
Action Filters Implementation
To create an Action filter, we need to create a class that inherits either from the IActionFilter
interface or IAsyncActionFilter
interface or from the ActionFilterAttribute
class which is the implementation of the IActionFilter
, IAsyncActionFilter
, and a few different interfaces as well:
public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
In our examples, we are going to inherit from the IActionFIlter
interface because it has all the method definitions we require.
To implement the synchronous Action filter that runs before and after action method execution, we need to implement OnActionExecuting
and OnActionExecuted
methods:
namespace ActionFilters.Filters { public class ActionFilterExample : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { // our code before action executes } public void OnActionExecuted(ActionExecutedContext context) { // our code after action executes } } }
We can do the same thing with an asynchronous filter by inheriting from IAsyncActionFilter
, but we only have one method to implement the OnActionExecutionAsync
:
namespace ActionFilters.Filters { public class AsyncActionFilterExample : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // execute any code before the action executes var result = await next(); // execute any code after the action executes } } }
The Scope of Action Filters
Like the other types of filters, the action filter can be added to different scope levels: Global, Action, Controller.
If we want to use our filter globally, we need to 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, we don’t have the Startup class, so we have to use the Program class:
builder.Services.AddControllers(config => { config.Filters.Add(new GlobalFilterExample()); });
But if we want to use our filter as a service type on the Action or Controller level, we need to register it in the same ConfigureServices
method but as a service in the IoC container:
services.AddScoped<ActionFilterExample>(); services.AddScoped<ControllerFilterExample>();
Or in .NET 6 and above:
builder.Services.AddScoped<ActionFilterExample>(); builder.Services.AddScoped<ControllerFilterExample>();
Finally, to use a filter registered on the Action or Controller level, we need to place it on top of the Controller or Action as a ServiceType:
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" }; } } }
Order of Invocation
The order in which our filters are executed is as follows:
Of course, we can change the order of invocation by adding an additional property Order
to the invocation statement:
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" }; } } }
Or something like this on top of the same action:
[HttpGet] [ServiceFilter(typeof(ActionFilterExample), Order=2)] [ServiceFilter(typeof(ActionFilterExample2), Order=1)] public IEnumerable<string> Get() { return new string[] { "example", "data" }; }
Improving the Code with Action Filters
If we open the starting project from the AppStart folder from our repository, we can find the MoveController
class in the Controllers
folder. This controller has an implementation for all the CRUD operations. For the sake of simplicity, we haven’t used any additional layers for our API. This project also implements global error handling so if you are not familiar with that topic, we suggest you read Global Exception Handling in .NET Core Web API.
Our actions are quite clean and readable without try-catch
blocks due to global exception handling, but we can improve them even further.
The important thing to notice 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; } }
So let’s start with the validation code from the POST and PUT actions.
Validation with Action Filters
If we look at our POST and PUT actions, we can notice the repeated code in which we validate our Movie
model:
if (movie == null) { return BadRequest("Movie object is null"); } if (!ModelState.IsValid) { return BadRequest(ModelState); }
We can extract that code into a custom Action Filter class, thus making this code reusable and the action cleaner.
So let’s do that.
Let’s create a new folder in our solution explorer, and name it ActionFilters
. Then inside that folder, we are going to create a new class ValidationFilterAttribute
:
using Microsoft.AspNetCore.Mvc.Filters; namespace ActionFilters.ActionFilters { public class ValidationFilterAttribute : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { } public void OnActionExecuted(ActionExecutedContext context) { } } }
Now we are going to modify the OnActionExecuting
method to validate our model:
using ActionFilters.Contracts; 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) { } } }
Next, let’s register this action filter 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 have 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, let’s remove that validation code from our actions and call this action filter 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(); }
Excellent.
This code is much cleaner and more readable now without the validation part. And furthermore, the validation part is now reusable as long as our model classes inherit from the IEntity interface, which is quite common behavior.
Before we test this validation filter, we have to suppress validation from the [ApiController]
attribute. If we don’t do it, it will overtake the validation from our action filter and always return 400 (BadRequest) for all validation errors. But as you’ve seen, if our model is invalid, we want to return the UnprocessableEntity result (422).
To suppress the default validation, we have to modify the Startup
class:
services.Configure<ApiBehaviorOptions>(options => { options.SuppressModelStateInvalidFilter = true; });
Or in .NET 6 in the Program class:
builder.Services.Configure<ApiBehaviorOptions>(options => { options.SuppressModelStateInvalidFilter = true; });
Now, if we send a PUT request for example with the invalid model we will get the Unprocessable Entity
response:
Dependency Injection in Action Filters
If we take a look at our GetById
, DELETE and PUT actions, we are going to see the code where we fetch the move by id from the database and check if it exists:
var dbMovie = _context.Movies.SingleOrDefault(x => x.Id.Equals(id)); if (dbMovie == null) { return NotFound(); }
That’s something we can extract to the Action Filter class as well, thus making it reusable in all the actions.
Of course, we need to inject our context
in a new ActionFilter class by using dependency injection.
So, let’s create another Action Filter class ValidateEntityExistsAttribute
in the ActionFilters
folder and modify it:
using System.Linq; 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) { } } }
We’ve created this Action Filter class to be generic so that we could reuse it for any model in our project. Furthermore, if we find the entity in the database, we store it in HttpContext
because we need that entity in our action methods and we don’t want to query the database two times (we would lose more than we gain if we double that action).
Now let’s register it:
services.AddScoped<ValidateEntityExistsAttribute<Movie>>();
Or in .NET 6 and above:
builder.Services.AddScoped<ValidateEntityExistsAttribute<Movie>>();
And let’s modify our actions:
[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(); }
Awesome.
Now our actions look great without code repetition, try-catch blocks, or additional fetch requests towards the database.
Conclusion
Thank you for reading this article. We hope you have learned new useful things.
As we already said, we always recommend using Action Filters because they give us reusability in our code and cleaner code in our actions as well.
Great article. Although I don’t think the ValidateEntityExistAttribute filter is reusable in another controller for instance, BookController because it already has _context field of type Movie. Or did I miss something?
Hi Ade. Maybe the name of the context class isn’t the greatest name here, but the context itself has nothing to do with the Movie type. Call it AppContext if you want, but you can reuse it. Check the code on GitHub and you will see this is nothing more or less than an ordinary context class. So, it can be reused for all the entities that use that context class in a project.
Okay, I see it now. Thanks a lot. Please keep up the great work!
Will do. Thank you.
Great updated article with .NET 6, just a typo:
https://code-maze.com/action-filters-aspnetcore/#:~:text=Acton%20filter
Hello Sergio. Thanks a lot for reading the article, and for that suggestion. We will fix that ASAP.
Could you please make a custom filter and implement it inside the code as simple as it can be…!
I am not sure what you mean by “custom filter”. All the action filters implemented in this article are custom ones and used as such.
Great article.
I tried this but the model state validation inside the filter onActionExecuting is not firing, which means
if(!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
this line of code is not executing anyway. i put break point here and make a invalid request to the action method but code execution didn’t reach in filter modelstate validtion .can you check this?
Hello Sarath. That probably happens because of the ApiController attribute. It takes over the control over the validation rules before your request reaches the action filter and your actions inside the controller. For testing purposes, you can comment it out. But the better solution is to suppress its validation in the Startup or Program class if you use .NET 6.
Thanks for the reply. Let me check to adf validation in the startup. Honestly i didn’t try this before, do you have any documents for the same? If you have please share that will help me alot
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
This code is for .NET 6. But for previous versions, you don’t have the builder just the services parameter. The rest is the same. This will suppress model state validation for the ApiController attribute.
Thank you so much 🥰
How would you catch a filter that sets a result. For example, i have one controller method that should return a 404 where the user filter normally returns a 401. So i basically need to catch the filter after it has set Context.Result.
Is it possible to achieve the same functionality with use custom attributes like ActionFilterAttribute?? Any way the article which you mentioned using iactionfilter which is great but the way we are using it kind of messy i know it a standard approach and those kind of staff but in compare of actionfilterattribute you can say it is.. any way great article. I learn something new for sure. Thanks🤗🤗🤗
Well, it is possible, as I stated at the beginning of the article: “To create an Acton filter, we need to create a class that inherits either from the IActionFilter interface or IAsyncActionFilter interface or from the ActionFilterAttribute class which is the implementation of the IActionFilter, IAsyncActionFilter, and a few different interfaces as well”.
Great article. Thanks
You are welcome. Thank you for reading.
very simple and very very effective. Thanks.
Thank you very much for reading the article. We have a lot of ASP.NET Core articles, so I hope you can find some more useful stuff for you. Have a great day.
Hi Marinko, great work 🙂
Questions:
What is a best practice for working with dtos and multiple entities? For example in AccountOwner project we have more “complex” solution. You suggested to write base abstract class for create/update dtos (which will replace IEntity in use of validating (
p => p.Value is IEntity
) or i miss something?) but still if we can’t group every dto/entity with corresponding abstract class how handle unique ones. Should we write multiple interfaces for handling each?In this example, we are using DbContext directly instead of using repository. Why?
And last one 🙂 If I understood correctly, filters can grow a lot if our intention is to handle every situation, so is it a good idea to put all filters in a separate project?
Hi Antonio.
I am not sure that I understand the first question. Where, in this article, did I suggest to use an Abstract class for create/update dtos? But if you have multiple entities that can’t be grouped under the IEntity interface, you can write multiple interfaces, but it doeasn’t make any sense if you use single interface for a single enitity. In that situation you can work on top of that enitty itself.
We didn’t use repository because it would add an additional complexity to the article. That was not the topic. That’s the only reason. In our book, we used Repository in our AF’s for sure.
If you want to put all the filters into a separate project you can do that. But if the only problem is that you have a lot of filter classes, then I would just place them in a separate folder. But if you want to reuse them, you should place them in a separate project.
Marinko, Thank you for answer. Its all clear now.
i didn’t refer to Basic ASP.NET Core Web API Guide Its my fault. Sorry
Great post
Thanks a lot.
Thank you. Was looking exactly to do this.
You are very welcome.
great ,just a question
how can i avoid Movie in
services.AddScoped>();
thanks
Hello. You can’t avoid Movie because this action filter is generic one (ValidateEntityExistsAttribute) so you need to provide it a model which inherits from IEntity interface (in this case it is the Movie model class). If you want to create another controller to work with CRUD or any other operations with Customer model for example, during the registration of the ValidationEntityExistsAttribute filter, you need to provide the Customer model as well (Of course it needs to implement IEntity interface).
very good
Thanks a lot. Hope you will find our other articles useful as well. All the best.