Customizing ASP.NET Core Part 9: ActionFilter
We take a look at making your own ActionFilter classes in order to keep Actions small and readable. Read on to started mastering ASP.NET Core!
Join the DZone community and get the full member experience.
Join For FreeThis post is a little late this time. My initial plan was to throw out two posts of this series per week, but this didn't work out since there are sometimes some more family and work tasks to do than expected.
Anyway, we keep on customizing on the controller level in this, the ninth, post of this blog series. I'll have a look into ActionFilters and how to create your own ActionFilter to keep your Actions small and readable.
Initial Series Topics
- Customizing ASP.NET Core Part 1: Logging
- Customizing ASP.NET Core Part 2: Configuration
- Customizing ASP.NET Core Part 3: Dependency Injection
- Customizing ASP.NET Core Part 4: HTTPS
- Customizing ASP.NET Core Part 5: HostedServices
- Customizing ASP.NET Core Part 6: Middleware
- Customizing ASP.NET Core Part 7: OutputFormatter
- Customizing ASP.NET Core Part 8: ModelBinders
- Customizing ASP.NET Core Part 9: ActionFilters - This article
- Customizing ASP.NET Core Part 10: TagHelpers
About ActionFilters
Action filters are a little bit like middleware, but are executed immediately on a specific action or on all actions of a specific controller. If you apply an ActionFilter as a global one, it executes on all actions in your application. ActionFilters are created to execute code right before the actions is executed or after the action is executed. They are introduced to execute aspects that are not part of the actual action logic. Authorization is such an aspect. I'm sure you already know the AuthorizeAttribute
to allow users or groups to access specific Actions or Controllers. The AuthorizeAttribute
actually is an ActionFilter. It checks whether the logged-on user is authorized or not. If not, it redirects to the log-on page.
The next sample shows the skeletons of a normal ActionFilters and an async ActionFilter:
public class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
}
}
public class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// do something before the action executes
var resultContext = await next();
// do something after the action executes; resultContext.Result will be set
}
}
As you can see here, there are always two sections to place code in to execute before and after the action is executed. This ActionFilters cannot be used as attributes. If you want to use the ActionFilters as attributes in your Controllers, you need to drive from Attribute or from ActionFilterAttribute
:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
This code shows a simple ActionFilter which always returns a BadRequestObjectResult
, if the ModelState
is not valid. This may be useful as a Web API or as a default check on POST, PUT, and PATCH requests. This could be extended with a lot more validation logic. We'll see how to use it later on.
Another possible use case for an ActionFilter is logging. You don't need to log in the Controllers and Actions directly. You can do this in an action filter to not mess up the actions with not relevant code:
public class LoggingActionFilter : IActionFilter
{
ILogger _logger;
public LoggingActionFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<LoggingActionFilter>();
}
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
_logger.LogInformation($"Action '{context.ActionDescriptor.DisplayName}' executing");
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
_logger.LogInformation($"Action '{context.ActionDescriptor.DisplayName}' executed");
}
}
This logs an informational message out to the console. You are able to get more information about the current Action out of the ActionExecutingContext
or the ActionExecutedContext
e.g. the arguments, the argument values, and so on. This makes the ActionFilters pretty useful.
Using the ActionFilters
ActionFilters that actually are Attributes can be registered as an attribute of an Action or a Controller:
[HttpPost]
[ValidateModel] // ActionFilter as attribute
public ActionResult<Person> Post([FromBody] Person model)
{
// save the person
return model; //just to test the action
}
Here we use the ValidateModelAttribute
that checks the ModelState
and returns a BadRequestObjectResult
in case the ModelState
is invalid and I don't need to check the ModelState
in the actual Action.
To register ActionFilters globally you need to extend the MVC registration in the CofnigureServices
method of the Startup.cs
:
services.AddMvc()
.AddMvcOptions(options =>
{
options.Filters.Add(new SampleActionFilter());
options.Filters.Add(new SampleAsyncActionFilter());
});
ActionFilters registered like this are getting executed on every action. This way you are able to use ActionFilters that don't derive from Attribute.
The Logging LoggingActionFilter
we created previously is a little more special. It is depending on an instance of an ILoggerFactory
, which need to be passed into the constructor. This won't work well as an attribute, because Attributes don't support constructor injection via dependency injection. The ILoggerFactory
is registered in the ASP.NET Core dependency injection container and needs to be injected into the LoggingActionFilter
.
Because of this, there are some more ways to register ActionFilters. Globally we are able to register it as a type, that gets instantiated by the dependency injection container and the dependencies can be solved by the container.
services.AddMvc()
.AddMvcOptions(options =>
{
options.Filters.Add<LoggingActionFilter>();
})
This works well. We now have the ILoggerFactory
in the filter
To support automatic resolution in Attributes, you need to use the ServiceFilterAttribute
on the Controller or Action level:
[ServiceFilter(typeof(LoggingActionFilter))]
public class HomeController : Controller
{
in addition to the global filter registration, the ActionFilter needs to be registered in the ServiceCollection
before we can use it with the ServiceFilterAttribute
:
services.AddSingleton<LoggingActionFilter>();
To be complete, there is another way to use ActionFilters that needs arguments passed into the constructor. You can use the TypeFilterAttribute
to automatically instantiate the filter. But, when using this attribute, the Filter isn't instantiated by the dependency injection container and the arguments need to get specified as an argument of the TypeFilterAttribute
. See the next snippet from the docs:
[TypeFilter(typeof(AddHeaderAttribute),
Arguments = new object[] { "Author", "Juergen Gutsch (@sharpcms)" })]
public IActionResult Hi(string name)
{
return Content($"Hi {name}");
}
The Type of the filter end the arguments are specified with the TypeFilterAttribute
Conclusion
Personally, I like the way we can keep the actions clean using ActionFilters. If I find repeating tasks inside my Actions that are not really relevant to the actual responsibility of the Action, I try to move it out to an ActionFilter, or maybe a ModelBinder or a middleware, depending on how globally it should work. The more it is relevant to an Action the more likely I am to use an ActionFilter.
There are some more kinds of filters, which all work similarly. To learn more about the different kind of filters, you definitely need to read the docs.
In the tenth part of the series, we move to the actual view logic and extend the Razor Views with custom TagHelpers. Customizing ASP.NET Core Part 10: TagHelpers.
Published at DZone with permission of Juergen Gutsch, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments