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!