Back to: ASP.NET Core Tutorials For Beginners and Professionals
Action Filters in ASP.NET Core MVC
In this article, I will discuss Action Filters in ASP.NET Core MVC Applications with Examples. Please read our previous article discussing Authorization Filters in ASP.NET Core MVC Applications.
What are Action Filters in ASP.NET Core MVC?
In ASP.NET Core MVC, action filters allow us to execute code before or after the execution of action methods in controllers. Action Filters are useful for handling cross-cutting concerns within an application, such as Logging, Authentication, Authorization, Caching, Exception Handling, etc.Â
How Do We Create a Custom Action Filter in ASP.NET Core MVC?
In ASP.NET Core, we can create a Custom Action Filter in two ways: First, by creating a class implementing the IActionFilter interface and providing implementations for [OnActionExecuting] and [OnActionExecuted] methods. Second, by creating a class inheriting from the ActionFilterAttribute class and overriding the [OnActionExecuting] and [OnActionExecuted] methods.
- OnActionExecuting:Â This method is called before the action method is executed
- OnActionExecuted:Â This method is called after the action method executes but before the result is processed.
Note: If you implement the IAsyncActionFilter interface, you need to provide implementations for the OnActionExecutionAsync method.
Real-Time Example to Understand Action Filters in ASP.NET Core MVC:
Let us create one Real-Time Application to understand the need and use of Custom Action Filters in ASP.NET Core MVC Application. We are going to develop one application where we will implement the following functionalities:
- Logging: Implementing logging of action method calls, parameters, execution times, etc.
- Data Transformation: Modifying the data passed to an action or returned from an action.
- Validation: Performing custom validation of action parameters or the request itself.
- Error Handling: Implementing custom error handling logic for actions.
- Caching: Implementing custom caching strategies for action results.
For each functionality, we will create a separate Custom Action Filter. I will also implement the Custom Action filter in different ways. I will also show you how to use Custom Services within the Custom Action filter. Also, I will discuss How to register the Custom Action Filter at different levels, i.e., Globally, Controller level and action method level. Finally, I will create one Controller with action methods showing the use of each Custom Action Filter:
Creating a Model:
Create a class file named MyCustomModel.cs within the Models folder and then copy and paste the following code into it. This will be our model, which we will use to return the data to the client and the action method parameter.
namespace FiltersDemo.Models { public class MyCustomModel { public string? Name { get; set; } public string? Address { get; set; } public void TransformData() { Name += " - Transformed"; Address += " - Transformed"; } } }
We will see how to Validate the above Model using a Custom Action Filter as well as we will see how to modify the above model data using a Custom Action Filter while returning the data to the client.
Creating a Logger Service:
Let us define our service interface and implementation for the logging. This Logger service is going to be used by our Custom Action Filters. So, create an interface named ILoggerService.cs within the Models folder and then copy and paste the following code:
namespace FiltersDemo.Models { public interface ILoggerService { public void Log(string message); } }
Next, create another class file named LoggerService.cs within the Models folder and copy and paste the following code. This class implements the ILoggerService interface and implements the Log method, where we have written the logic to store the log message in a text file.
namespace FiltersDemo.Models { public class LoggerService : ILoggerService { public void Log(string message) { string filePath = Path.Combine(Directory.GetCurrentDirectory(), "Log", "Log.txt"); //saving the data in a text file called Log.txt within the Log folder which must be //created at the Project root directory File.AppendAllText(filePath, message); } } }
Creating the Log Folder:
Next, create a folder called Log within the Project root directory where the Log.txt file is going to be generated by the application.
Registering the Logger Service:
Next, we need to register the Logger Service into the built-in dependency injection container. This is because we want to use the Logger service through our application, including the Custom Action Filter, and we want the Framework to inject the logger service through the constructor. So, add the following code to the Program.cs class file:
builder.Services.AddSingleton<ILoggerService, LoggerService>();
Creating a Custom Action Filter for Logging in ASP.NET Core MVC:
Create a class file named LoggingFilterAttribute.cs within the Models folder, and then copy and paste the following code. This filter logs details about the action method calls, including parameters, execution times, etc.
using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; using System.Diagnostics; namespace FiltersDemo.Models { public class LoggingFilterAttribute : ActionFilterAttribute { private Stopwatch? _timer = null; private readonly ILoggerService _LoggerService; public LoggingFilterAttribute(ILoggerService LoggerService) { _LoggerService = LoggerService; } public override void OnActionExecuting(ActionExecutingContext context) { _timer = Stopwatch.StartNew(); var actionName = context.ActionDescriptor.RouteValues["action"]; var controllerName = context.ActionDescriptor.RouteValues["controller"]; var parameters = JsonConvert.SerializeObject(context.ActionArguments); string message = $"Starting {controllerName}.{actionName} with parameters {parameters}"; _LoggerService.Log(message); base.OnActionExecuting(context); } public override void OnActionExecuted(ActionExecutedContext context) { _timer?.Stop(); var actionName = context.ActionDescriptor.RouteValues["action"]; var controllerName = context.ActionDescriptor.RouteValues["controller"]; string message = $"Finished {controllerName}.{actionName} in {_timer.ElapsedMilliseconds}ms"; _LoggerService.Log(message); base.OnActionExecuted(context); } } }
The above LoggingFilterAttribute is a Custom Filter that inherits from ActionFilterAttribute. This class is used to log information about MVC actions, specifically when they start and when they finish. It uses two methods, OnActionExecuting and OnActionExecuted, to achieve this.
OnActionExecuting Method
The OnActionExecuting method is called before the action method is executed. This method is used for the following purposes:
- Start a Timer: It initializes and starts the Stopwatch to measure the duration of the action’s execution. This is important for logging the action’s execution time.
- Preparing the Log Message: It constructs a message that includes the controller’s name, the action’s name, and the action’s serialized arguments.
- Log the Start of the Action: The constructed message (indicating the start of action execution) is logged using the _LoggerService. This helps in tracking when an action begins its execution.
OnActionExecuted Method
The OnActionExecuted method is invoked after the action method has been executed. This method is used for the following purposes:
- Stop the Timer: It stops the Stopwatch that was started in OnActionExecuting.
- Preparing the Log Message: Similar to OnActionExecuting, it constructs a message that includes the name of the controller, the action, and the time taken (in milliseconds) to execute the action.
- Logging the End of the Action: This message (indicating the completion of the action and its duration) is then logged using the same logging service.
Note: Both methods call base.OnActionExecuting(context) and base.OnActionExecuted(context) at the end of their implementations. These calls ensure that any additional processing defined in the base class (ActionFilterAttribute) is also executed.
Creating a Custom Action Filter for Data Transformation in ASP.NET Core MVC:
Create a class file named DataTransformationFilterAttribute.cs within the Models folder, and then copy and paste the following code. This filter modifies the data returned from an action method. It assumes you are returning a specific model that could be transformed. Here, the following class is inherited from the ActionFilterAttribute and overrides the OnActionExecuted method.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; namespace FiltersDemo.Models { public class DataTransformationFilterAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Result is ViewResult viewResult) { if (viewResult.Model is MyCustomModel model) { // Directly modify the model data model.TransformData(); } } base.OnActionExecuted(context); } } }
Understanding the OnActionExecuted method:
- Check the Result Type: The method first checks if the action method’s result (context.Result) is of type ViewResult. ViewResult is a type of action result that renders a view as the response to the request.
- Access and Modify the Model: If the result type is ViewResult, the method then checks if the model associated with this view result is of type MyCustomModel. If it is, it accesses this model.
- Transform the Model Data: Once it has access to the model, the method calls the TransformData() function on the model. This function is assumed to manipulate or transform the data within the model. Depending on the implementation of TransformData(), this could involve changing values, formatting data, calculating fields, etc.
- Call Base Method: Finally, the method calls base.OnActionExecuted(context), which ensures that any logic implemented in the base class’s OnActionExecuted method is also executed.
Creating a Custom Action Filter for Validation in ASP.NET Core MVC:
Create a class file named CustomValidationFilter.cs within the Models folder, and then copy and paste the following code. This filter performs custom validation of action parameters. Here, we created the Custom Action Filter, implementing the IActionFilter interface and providing implementations for the OnActionExecuting and OnActionExecuted methods. Actually, we are not doing anything on the OnActionExecuted methods.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace FiltersDemo.Models { public class CustomValidationFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { if (context.ActionArguments.TryGetValue("model", out var value) && value is MyCustomModel model) { // Validate Name if (string.IsNullOrWhiteSpace(model.Name)) { context.ModelState.AddModelError(nameof(model.Name), "Name cannot be empty or whitespace."); } // Validate Address if (string.IsNullOrWhiteSpace(model.Address)) { context.ModelState.AddModelError(nameof(model.Address), "Address cannot be empty or whitespace."); } } if (!context.ModelState.IsValid) { // Assuming the controller action expects a return of the same view with the model context.Result = new ViewResult { ViewName = context.RouteData.Values["action"].ToString(), // Gets the action name ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), context.ModelState) { Model = context.ActionArguments.FirstOrDefault(arg => arg.Value is MyCustomModel).Value } }; } } public void OnActionExecuted(ActionExecutedContext context) { // Custom logic after the action executes // For this filter, we do nothing here, but you could add post-processing logic if needed. } } }
Understanding OnActionExecuting Method
- Argument Validation: The method starts by attempting to retrieve an argument named “model” from the context’s ActionArguments. It then checks if this argument is of type MyCustomModel.
- Name Validation: If the model’s Name property is null or whitespace, a model error stating that “Name cannot be empty or whitespace” is added.
- Address Validation: Similarly, it checks the Address property and adds a model error if it’s null or whitespace.
- Invalid Model Handling: If the model state is invalid, it prevents the execution of the action method by setting the context’s Result property.
- Creating and Returning ViewResult: A new ViewResult is created to return the same view with the invalid model data. This ViewResult includes the name of the action (retrieved from context.RouteData) to indicate which view to return. A new ViewDataDictionary is initialized with the model state and model, allowing the view to display validation errors.
Creating a Custom Action Filter for Error Handling in ASP.NET Core MVC:
Create a class file named ErrorHandlerFilterAttribute.cs within the Models folder, and then copy and paste the following code. This filter implements custom error-handling logic for actions. The Custom Action Filter is inherited from the ExceptionFilterAttribute class and overrides the OnException method.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace FiltersDemo.Models { public class ErrorHandlerFilterAttribute : ExceptionFilterAttribute { private readonly ILoggerService _LoggerService; public ErrorHandlerFilterAttribute(ILoggerService LoggerService) { _LoggerService = LoggerService; } public override void OnException(ExceptionContext context) { string message = $"An error occurred in {context.ActionDescriptor.DisplayName}: {context.Exception}"; _LoggerService.Log(message); // Set the result to redirect to the generic error view context.Result = new ViewResult { ViewName = "~/Views/Shared/Error.cshtml", // Explicit path to the view ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), context.ModelState) { {"Exception", context.Exception} // Optionally pass exception data to the view } }; context.ExceptionHandled = true; // Mark exception as handled } } }
Understanding OnException Method:
- Logging the Exception: When an exception occurs in any action method to which this filter is applied, the OnException method captures the exception and logs a detailed message. The message includes the name of the action where the exception occurred and the exception details itself. This is done by the Logger Service, which is injected through the constructor.
- Setting the Response: After logging the exception, the method proceeds to alter the user’s experience by redirecting them to a generic error page. This is done by setting context.Result to a new ViewResult:
- View Name: It specifies the path to the error view (~/Views/Shared/Error.cshtml). This ensures that whenever an exception is handled by this filter, the user is redirected to a standard error page, maintaining a consistent error-handling strategy across the application.
- ViewData: The ViewDataDictionary is populated with the exception data using the EmptyModelMetadataProvider and the current ModelState. This allows the error view to access details of the exception, if necessary, which can be useful for displaying error messages or diagnostic information on the error page.
- Marking the Exception as Handled: By setting context.ExceptionHandled to true, the filter communicates to the ASP.NET Core framework that the exception has been handled. This prevents the exception from propagating further, which means it won’t trigger other exception handlers or result in the framework’s default error-handling mechanisms taking over (such as showing the developer exception page).
Creating the Error View:
Next, create a view named Error.cshtml within the Views/Shared folder and then copy and paste the following code. This is the view which is going to be rendered by the above Custom Error Handler:
@{ Layout = "~/Views/Shared/_Layout.cshtml"; ViewData["Title"] = "Error"; } <h1 class="text-danger">Oops! Something went wrong.</h1> <p>We're having trouble processing your request. Please try again later.</p>
Creating a Custom Action Filter for Caching in ASP.NET Core MVC:
Create a class file named AsyncCachingFilter.cs within the Models folder, and then copy and paste the following code. The following Custom Action Filter implements the IAsyncActionFilter and provides an implementation for the OnActionExecutionAsync method.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; namespace FiltersDemo.Models { public class AsyncCachingFilter : IAsyncActionFilter { private readonly IMemoryCache _cache; private readonly TimeSpan _expirationTimeSpan; public AsyncCachingFilter(IMemoryCache cache, double secondsToCache = 60) { _cache = cache; _expirationTimeSpan = TimeSpan.FromSeconds(secondsToCache); } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var key = $"{context.HttpContext.Request.Path}"; if (_cache.TryGetValue(key, out IActionResult cachedResult)) { context.Result = cachedResult; // Return cached result } else { // Proceed with the action execution var executedContext = await next(); // Cache any IActionResult that is successfully returned if (executedContext.Result is ActionResult actionResult) { _cache.Set(key, actionResult, _expirationTimeSpan); } } } } }
Understanding OnActionExecutionAsync Method:
- Initialization: The Constructor uses an injected IMemoryCache instance (_cache) to store and retrieve cached data. The cache’s expiration is set via a TimeSpan (_expirationTimeSpan), which is determined by the number of seconds passed into the constructor.
- Caching Logic: The cache key is generated from the request path (context.HttpContext.Request.Path). This ensures that each unique URL has its own cache entry.
- Cache Lookup: The method first checks if there is a cached result available for the generated key using _cache.TryGetValue(key, out IActionResult cachedResult). If a cached result exists (cachedResult), it is immediately assigned to context.Result. This tells the framework to skip executing the action method and return the cached result directly to the client.
- Action Execution: If no cached result is found, the method proceeds to execute the action by calling await next(). This ActionExecutionDelegate (next) represents the next delegate in the action filter pipeline, which typically leads to the execution of the action method itself. After the action executes, the resulting context (executedContext) is returned, which includes the result of the action method.
- Caching the Result: The result of the action method (executedContext.Result) is checked to see if it’s an instance of ActionResult. This check is important because only action results should be cached. If it is an ActionResult, it is added to the cache with the earlier generated key. The cache entry is set to expire based on _expirationTimeSpan.
Modifying the Home Controller:
Let’s modify the Home Controller class code as follows to demonstrate the use of all created Custom Action Filters.
using FiltersDemo.Models; using Microsoft.AspNetCore.Mvc; namespace FiltersDemo.Controllers { [TypeFilter(typeof(LoggingFilterAttribute))] public class HomeController : Controller { [TypeFilter(typeof(AsyncCachingFilter))] public IActionResult Index() { // Storing the current time in ViewBag ViewBag.CurrentTime = DateTime.Now; return View(); } [DataTransformationFilter] public IActionResult Details() { var model = new MyCustomModel { Name = "Initial Name", Address = "Initial Address" }; //return Ok(model); return View(model); } [HttpGet] public IActionResult Create() { return View(); } [TypeFilter(typeof(CustomValidationFilter))] [HttpPost] public IActionResult Create(MyCustomModel model) { if (ModelState.IsValid) { // Process the valid model here return RedirectToAction(nameof(Index)); } return View(model); } [TypeFilter(typeof(ErrorHandlerFilterAttribute))] public IActionResult Error() { throw new Exception("This is a forced error!"); } } }
Creating and Modifying the Views:
Next, we need to modify and create the Views as per our requirements:
Index.cshtml View:
Modify the Index.cshtml View as follows:
@{ ViewData["Title"] = "Index Page"; } <h2>Current Server Time</h2> <p>The current server time is: @ViewBag.CurrentTime.ToString("F")</p>
Details.cshtml View
Add Details.cshtml View within the Views/Home folder and then copy and paste the following code:
@model FiltersDemo.Models.MyCustomModel @{ ViewData["Title"] = "Details Page"; } <div class="container mt-5"> <h1 class="mb-3">Details</h1> <div class="card"> <div class="card-body"> <h5 class="card-title">Name</h5> <p class="card-text">@Model.Name</p> <h5 class="card-title">Address</h5> <p class="card-text">@Model.Address</p> </div> </div> </div>
Create.cshtml View
Add Create.cshtml View within the Views/Home folder and then copy and paste the following code:
@model FiltersDemo.Models.MyCustomModel @{ ViewData["Title"] = "Create Model"; } <div class="container mt-5"> <h1>Create Model</h1> <form asp-action="Create" method="post" class="needs-validation" novalidate> <div asp-validation-summary="ModelOnly" class="alert alert-danger"></div> <div class="form-group"> <label for="Name">Name</label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="form-group"> <label for="Address">Address</label> <input asp-for="Address" class="form-control" /> <span asp-validation-for="Address" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div>
Testing the Application:
Now, run the application and test each action method. We have applied the Logging Custom Filter at the Controller level, meaning it will be applied to all Action methods of the Home Controller.
Index Action Method:
Now, access the Home/Index URL, and you should see the following. Now, within 60 seconds, if you access the same page, then you will see that the Date is not going to be changed. This is because we have applied the Custom Cache filter on the Index Action method:
Details Action Method:
Now, access the Home/Details URL, and you should see the following. This is because we have applied the Custom Data Modification filter on the Details Action method, which modifies the Model:
Create Action Method:
Now, access the Home/Create URL without providing any data, click on the Submit button, and you should see the following. This is because we have applied the Custom Validator filter on the Create Post Action method, which is doing the Model validation before executing the Create Post action method:
Error Action Method:
Now, access the Home/Error URL, and you should see the following. This is because we have applied the Custom Exception filter to the Error Action method. The Error action method throws an unhandled exception that is going to be handled by the Custom Exception Filter, and then it returns a generic error page to the client.
In the next article, I will discuss the Difference Between TypeFilter and ServiceFilter in ASP.NET Core MVC Applications. In this article, I try to explain the Action Filters in ASP.NET Core MVC Applications with Examples. I hope you understand the need and use of Action Filters in the ASP.NET Core MVC Applications.