Skip to content

Simple full-page caching in nopCommerce

Sometimes, it’s all about performance! Especially if your nopCommerce site is high-traffic and / or you desire to minimize your hosting fees.

nopCommerce has a good amount of caching for PDPs and other db-intensive pages built-in. This can be quickly improved though, particularly for high request pages, by caching the entire view result. Here’s how you’d accomplish this in the nopCommerce 4.2 environment.

First, we create a type to cache.

using Microsoft.AspNetCore.Mvc ;
using Microsoft.AspNetCore.Mvc.ViewFeatures ;

    public class ActionResultCacheItem <U >
    {
        public ActionResultCacheItem ( )
        {
        }

        public ActionResultCacheItem (IActionResult result )
        {
            Model = GetModel <U > (result ) ;
            ViewName = GetViewName ( ) ;
        }

        public string ViewName { get ; set ; }
        public U Model { get ; set ; }

        public T GetResult <T > ( ) where T : ViewResult, new ( )
        {
            var viewDataDictionary =
                new ViewDataDictionary <U > ( new Microsoft . AspNetCore . Mvc . ModelBinding . EmptyModelMetadataProvider ( ),
                    new Microsoft . AspNetCore . Mvc . ModelBinding . ModelStateDictionary ( ) ) { } ;
            viewDataDictionary . Model = Model ;

            var viewResult = new T
            {
                ViewData = viewDataDictionary,
                ViewName = this . ViewName
            } ;

            return viewResult ;
        }

//instance member for demonstration, in-house we use an extension method.
    public T GetModel <T > (IActionResult actionResult )
        {
            object model = null ;
            if (actionResult is ViewResult @ base )
            {
                var viewResult = @ base ;
                model = viewResult . Model ;
            }
            else if (actionResult is ContentResult )
            {
                var @contentBase = (ContentResult )actionResult ;
                model = @contentBase . Content ;
            }
            else
            {
                return default (T ) ;
            }

            T typedModel ;
            try
            {
                typedModel = (T )model ;
            }
            catch
            {
                return default (T ) ;
            }

            return typedModel ;
        }


        public string GetViewName (IActionResult actionResult )
        {
            if (actionResult is ViewResult @ base )
            {
                return @ base . ViewName ;
            }

            return string . Empty ;
        }

    }

Next, using .Net ASP MVC Core ActionFilters we can intercept the action before and after it reaches the controller. If “OnActionExecuting” we find a cached ActionResult, we can prevent the controller action from running by assigning the cached item to the filter context’s result. If it’s a no-hit, we’ll run into our “OnActionExecuted” which executes immediately after the controller, where we cache the result.

Warning: You have to be very careful to account for page variations (roles, stores) in the cache key, and other things like “Recently viewed products,” “update cart items”, etc, which isn’t demonstrated here.

public class MyProductDetailsFilter : ActionFilterAttribute {
    public override void OnActionExecuted (ActionExecutedContext filterContext )
{
            base . OnActionExecuted (filterContext ) ;
            var controllerName = ( string )context . RouteData . Values [ "controller" ] ;
            var actionName = ( string )context . RouteData . Values [ "action" ] ;
            var allowFilterToExecute = controllerName . Equals ( "Product", StringComparison . CurrentCultureIgnoreCase ) && actionName . Equals ( "ProductDetails", StringComparison . CurrentCultureIgnoreCase ) ;

            if ( !allowFilterToExecute )
            {
                return ;
            }

            var model = filterContext . Result . GetModel <ProductDetailsModel > ( ) ;

            if (model == null ) { return ; }

            //cache  result
                var pdpResult = new ActionResultCacheItem <ProductDetailsModel > (filterContext . Result ) ;
                var cacheKey = string . Format ( "my-product-details-cache-key-{0}", pdpResult . Model . Id ) ;
                staticCacheManager . Set (cacheKey, pdpResult, int . MaxValue ) ;
           
        }


  public override void OnActionExecuting (ActionExecutingContext filterContext )
        {
            base . OnActionExecuting (filterContext ) ;
 
                    var controllerName = ( string )context . RouteData . Values [ "controller" ] ;
            var actionName = ( string )context . RouteData . Values [ "action" ] ;
            var allowFilterToExecute = controllerName . Equals ( "Product", StringComparison . CurrentCultureIgnoreCase ) && actionName . Equals ( "ProductDetails", StringComparison . CurrentCultureIgnoreCase ) ;

            if ( !allowFilterToExecute )
            {
                return ;
            }

            var staticCacheManager = EngineContext . Current . Resolve <IStaticCacheManager > ( ) ;
           
            if (filterContext . ActionArguments . ContainsKey ( "productId" ) && filterContext . ActionArguments [ "productId" ] != null )
            {
                var prodId = Convert . ToInt32 (filterContext . ActionArguments [ "productId" ] ) ;

             
                var cacheKey = string . Format ( "my-product-details-cache-key-{0}", prodId ) ; //you'd get much more fancy with this
                var cachedModel = staticCacheManager . Get <ActionResultCacheItem <ProductDetailsModel >> (cacheKey, ( ) => null ) ;
                if (cachedModel != null )
                {
                    var result = cachedModel . GetResult <ViewResult > ( ) ;
                    filterContext . Result = result ;
                  }
        }
}

Finally, register the filter in your nop startup routine.

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

        public int Order => 99999 ;
    }

And viola! You’ve just given your nopCommerce 4.2 (ASP .NET Core 2.2) site a powerful boost that should keep it smooth sailing when the traffic spikes!