How to create a Custom Action Filter in a nopCommerce v4 plugin

With Action Filters in ASP.NET Core you can alter the behavior of an Action Method before or after the method is called. You can use it to alter the arguments passed to the method or the returned result, also if you want to take some action when an Action Method is called, for example logging something everytime someone adds a product to the cart.

Action Filters can come in very handy in nopCommerce development when there is a need to extend or manipulate nopCommerce but without touching the core. In a nopCommerce plugin you can easily create a custom Action Filter and add it to the ASP.NET filter pipeline.

Creating the Action Filter

In this simple example we are going to create a custom Action Filter that manipulates the product names before the user loads the product details page. Perhaps not the most useful Action Filter but you will get an understanding of how they are created.

To create the Action Filter we need to perform following steps:

  1. Create a class that derive from ActionFilterAttribute.
  2. Implement the code that manipulates the products name.
  3. Add the filter to the ASP.NET filter pipeline.

1. Create the class

Let's get started by creating a new class ProductDetailsActionFilter in a plugin (for convenience I usually put all filters in Filters folder in root of the plugin). Let the class derive from ActionFilterAttribute.

Filters/ProductDetailsActionFilter.cs

    public class ProductDetailsActionFilter : ActionFilterAttribute
    {        
    }

ActionFilterAttribute has 5 methods that can be overridden in order to hook into various stages of the call to an Action Method:

  1. OnActionExecuting is called before the action is executed.
  2. OnActionExecuted is called after the action is executed.
  3. OnResultExecuting is called before the action's result is executed.
  4. OnResultExecuted is called after the action's result is executed.
  5. OnActionExecutionAsync is called asynchronously before the action is executed, with this method you can execute code both before and after the action is executed.

We want to manipulate the product name before the result is executed and we will need to do this in the OnResultExecuting method

public override void OnResultExecuting(ResultExecutingContext context)  
{
    // TODO only proceed if we are on the ProductDetails Action Method in the ProductController

    var result = context.Result as ViewResult;
    if (result == null) return;
    var model = result.Model as ProductDetailsModel;
    if (model == null) return;
    model.Name += " - This is magic from a Custom Action Filter";
}

From the passed ResultExecutionContext we can access the action result. In this case the result will be of type ViewResult since that is what's returned from ProductDetails action method if everything went well. The null check is important since a method could return something else if the code does not take the expected path, in this case if a product is not found a RedirectToRoute result is returned which would casuse our casting to ViewResult to be null and we want to stop executing our filter. Next we need to get hold of the returned model and cast it to ProductDetailsModel. From the model we can manipulate the name property.

Our filter will be called everytime any Action Method in our application is called. We need make sure we only proceeed executing our filter only if the ProductDetails Action Method on the ProductController is called. We can do that by inspecting the ResultExecutingContext that gets passed to our OnResultExecuting method.

public override void OnResultExecuting(ResultExecutingContext context)  
{
    if (!(context.ActionDescriptor is ControllerActionDescriptor actionDescriptor)) return;

    if (actionDescriptor.ControllerTypeInfo != typeof(ProductController) ||
        actionDescriptor.ActionName != "ProductDetails" ||
        context.HttpContext.Request.Method != "GET")
    {
        return;
    }

    var result = context.Result as ViewResult;
    if (result == null) return;
    var model = result.Model as ProductDetailsModel;
    if (model == null) return;
    model.Name += " - This is magic from a Custom Action Filter";
}
if (!(context.ActionContext.ActionDescriptor is ControllerActionDescriptor actionDescriptor)) return;  

if this code look unfamiliar it is the pattern matching feature in C# 7. It is just a shorter way of typing

var actionDescriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;  
if (actionDescriptor == null) return;  

From the action descriptor we can get the controller currently called by actionDescriptor.ControllerTypeInfo. If this is the ProductController we know we are in the right controller

 actionDescriptor.ControllerTypeInfo == typeof(ProductController)

Now we need the check if it is the ProductDetails method that is called by looking at the ActionName property

 actionDescriptor.ActionName == "ProductDetails"

Optionally (in this case) we can also check that the current request is of the desired HTTP method (GET). Since the ProductDetails method only exists for GET requests this check is not needed. But it's common that an Action Method exists for multiple HTTP methods, and this is how to distinguish them

 context.HttpContext.Request.Method == "GET"

3. Add the filter to the ASP.NET filter pipeline

We can easily add the filter to the ASP.NET filter pipeline by creating a class that inherits nopCommmerce INopStartup interfaace

public class NopStartup : INopStartup  
{
    public void ConfigureServices(IServiceCollection services, IConfigurationRoot configuration)
    {
        services.Configure<MvcOptions>(options =>
        {
            options.Filters.Add<ProductDetailsActionFilter>();
        });
    }

    public void Configure(IApplicationBuilder application)
    {

    }

    public int Order => 0;
}

That is it. If we run the application and navigates to a product, the name should have been manipulated like this:

As mentioned earlier this was just a very simple example of a custom action filter, from here you can do all kinds of things without having to touch the core of nopCommerce which will make future upgraded much easier.

Download complete code from this Gist: https://gist.github.com/martingust/d5cc3204ba505ae6472335ff32dbc8eb

"

Leave your comment