Kentico 12: Design Patterns Part 17 - Centralized Cache Management through Decoration
Sean G. Wright
Posted on November 4, 2019
Caching and Querying: A Primer
We want our Kentico 12 MVC applications to be fast ๐!
Database access, especially when there is a lot of it, can be slow ๐ฆฅ... and it also doesn't scale well as traffic to a site increases.
This is why we rely on caching as a means of keeping data closer to the application, or closer to the visitor of the site, where it's faster to access.
With our Kentico sites there is typically 2 places we can control cache within the application - data caching and output caching.
I've previously written about both data caching (query caching in particular) and output caching ๐ง:
Kentico 12: Design Patterns Part 12 - Database Query Caching Patterns
Sean G. Wright ใป Aug 26 '19 ใป 13 min read
Kentico 12: Design Patterns Part 15 - Output Caching and User Context
Sean G. Wright ใป Oct 14 '19 ใป 13 min read
So far, however, I've treated these topics as separate.
If we were to implement both query caching and output caching, as described by the posts above, we'd end up generating cache dependency keys twice - once for the data being queried from the database and once for the rendered views in the output cache.
This is not ideal, as it implies:
- โ We have 2 sets of cache keys being generated for the same data
- โ We potentially have 2 implementations for generating those keys
- โ The MVC layer, somehow, needs to know exactly which data a page depends on, which feels like a leaky abstraction
But what can we do about it? These two caching layers are, well, two separate layers! ... or are they ๐ฎ?
To put this in concrete terms lets look at an example using some of the strategies outlined in the above posts.
Query & Output: Separate Caching Strategies
IQuery and IQueryHandler
In my previous post, Database Query Caching Patterns, we ended up with a IQuery<TResponse>
/IQueryHandler<TQuery, TResponse>
pattern that is shown below:
public interface IQuery<TResult> { }
public interface IQueryHandler<TQuery, TResult>
where TQuery : IQuery<TResult>
{
TResult Exceute(TQuery query);
}
We implemented these interfaces with a feature based on retrieving Article
pages from the database, using some parametrization.
First we created an IQuery<TResponse>
implementation which represented the operation we wanted to execute - querying for Article
s based on the current SiteName
, Culture
, and Count
of items we wanted to retrieve:
public ArticlesQuery : IQuery<IEnumerable<Article>>
{
public int Count { get; set; }
public string SiteName { get; set; }
public string Culture { get; set; }
}
Then we created an implementation of the operation, using IQueryHandler<TQuery, TResponse>
, which wrapped a call to our ArticleProvider
, using the ArticlesQuery
parameters ๐ค:
public ArticlesQueryHandler : IQueryHandler<ArticlesQuery, IEnumerable<Article>>
{
public IEnumerable<Article> Execute(ArticlesQuery query)
{
return ArticleProvider.GetArticles()
.OnSite(query.SiteName)
.Culture(query.Culture)
.TopN(query.Count)
.OrderByDescending("DocumentPublishFrom")
.TypedResult;
}
}
The code could be used as follows:
I'm doing this in a
Controller
class for demonstration only.Controller
classes should be thin โโ - delegate this kind of work to other parts of your application.
public class ArticleController : Controller
{
private readonly IQueryHandler<ArticlesQuery, IEnumerable<Article>> handler;
private readonly ISiteContext siteContext;
private readonly ICultureContext cultureContext;
public ArticleController(
IQueryHandler<ArticlesQuery, IEnumerable<Article>> handler,
ISiteContext siteContext,
ICultureContext cultureContext)
{
this.handler = handler;
this.siteContext = siteContext;
this.cultureContext = cultureContext;
}
public ActionResult Index(int count)
{
var query = new ArticlesQuery
{
Count = count,
Culture = cultureContext.cultureName,
SiteName = siteContext.siteName
};
var articles = handler.Execute(query);
return View(articles);
}
}
Now we have our core data access patterns defined, so let's move on to the caching part ๐.
Query Caching
The reason we picked this IQuery
/IQueryHandler
pattern was due to its adherence to SOLID design principles.
Specifically:
- โ
Single Responsibility:
IQuery
defines data,IQueryHandler
defines implementation - โ Open/Closed: Easy to apply Aspect Oriented Programming through Decoration
- โ
Liskov Substitution: We can supply any implementation of our
IQueryHandler
to the aboveArticleController
- โ
Interface Segregation:
IQueryHandler
has 1 method:Execute()
- โ
Dependency Inversion: Interfaces, like
IQueryHandler
allow for query implementations to be supplied by the application at runtime - no concrete dependencies in our business logic
We created an additional type, IQueryCacheKeysCreator<TQuery, TResponse>
, that would generate the cache keys, and cache item name parts, for a given query:
public interface IQueryCacheKeysCreator<TQuery, TResult>
where TQuery : IQuery<TResult>
{
string[] DependencyKeys(TQuery query, TResult result);
object[] ItemNameParts(TQuery query);
}
This infrastructure helps us avoid the messy use of Attributes for string building, since that normally requires tokenization of strings to mark spots for replacement, ex:
[CacheKeys("nodes|##SITE_NAME##|Sandbox.Article|all")]
public class ArticlesQuery
{
// ...
}
Ensuring that
##SITE_NAME##
is in the correct place in the above string, doesn't have any typos, and works with all of the other tokens we might need to replace... sounds daunting and really error prone ๐ฑ.Just look at all the variable cache key parts in Kentico's documentation to get an idea of how complex this can get.
Our implementation of IQueryCacheKeysCreator
for the ArticlesQuery
is pretty simple, can take it's own constructor dependencies if needed, and can scale to more complex queries pretty easily ๐:
We could be injecting
ISiteContext
andICultureContext
as dependencies instead of passing that data through theArticlesQuery
, which is the approach I normally take ๐.
public class ArticlesQueryCacheKeysCreator :
IQueryCacheKeysCreator<ArticlesQuery, IEnumerable<Article>>
{
public string[] DependencyKeys(
ArticlesQuery query,
IEnumerable<Article> result) =>
new object[]
{
$"nodes|{query.SiteName}|{Article.CLASS_NAME}|all"
};
public object[] ItemNameParts(ArticlesQuery query) =>
new []
{
"myapp|data|articles", query.SiteName,
query.Culture, query.Count.ToString()
};
}
If you find yourself generating a lot of cache dependency keys by hand, check out my FluentCacheKeys NuGet package โก:
Kentico CMS Quick Tip: FluentCacheKeys - Consistent Cache Dependency Key Generation
Sean G. Wright for WiredViews ใป Sep 2 '19 ใป 5 min read
#kentico #cms #caching #csharp
The end-goal of all this architecture is to create a central point through which all queries and query responses will pass. In this case, it's the IQueryHandler
interface that all implementations must fulfill.
So, let's use Decoration as a means of intercepting access to (or applying an Aspect on) our IQueryHandler
implementations. This is where we do our caching:
public class QueryHandlerCacheDecorator<TQuery, TResponse>
: IQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
private readonly ICacheHelper;
private readonly IQueryHandler<TQuery, TResponse> handler;
private readonly IQueryCacheKeysCreator<TQuery, TResponse> cacheKeysCreator;
public QueryHandlerCacheDecorator(
ICacheHelper cacheHelper,
IQueryHandler<TQuery, TResponse> handler,
IQueryCacheKeysCreator<TQuery, TResponse> cacheKeysCreator)
{
this.cacheHelper = cacheHelper;
this.handler = handler;
this.cacheKeysCreator = cacheKeysCreator;
}
public TResponse Execute(TQuery query) =>
cacheHelper.Cache(
(cacheSettings) =>
{
TResponse result = handler.Execute(query);
if (cacheSettings.Cached)
{
cacheSettings.GetCacheDependency = () =>
cacheHelper.GetCacheDependency(
cacheKeysCreator.DependencyKeys(query, result));
}
return result;
},
new CacheSettings(
cacheMinutes: 10,
useSlidingExpiration: true,
cacheItemNameParts: cacheKeysCreator.ItemNameParts(query)));
}
Thanks to C# generics we get wonderful, strong-type support, and thanks to our Inversion of Control (IoC) container, we get to lay this QueryHandlerCacheDecorator
in front of every IQueryHandler
implementation with a simple configuration call ๐คฏ.
Using Autofac, the call would look like this:
var builder = new ContainerBuilder();
builder.RegisterGenericDecorator(
typeof(QueryHandlerCacheDecorator<,>),
typeof(IQueryHandler<,>));
Output Caching
Output caching, on the surface, is pretty simple ๐คจ.
ASP.NET MVC gives us the [OutputCache]
attribute that we can apply to any Controller
class or action method.
It has a handful of configuration options, like cache duration, what cache configuration from App Settings in the web.config
should be used.
You can read more about how to configure output caching in Kentico's documentation for caching in MVC applications, or Microsoft's documentation on enabling output caching.
The question we didn't look at in my previous post was this: When content in the CMS is changed, how is the output cache cleared correctly ๐ค?
Kentico's documentation shows the APIs that must be used to ensure we tell ASP.NET what the cache dependency keys are for a given output-cached page:
string dependencyCacheKey = String.Format(
"nodes|mvcsite|{0}|all",
Article.ClassName.ToLowerInvariant());
CacheHelper.EnsureDummyKey(dependencyCacheKey);
HttpContext.Response.AddCacheItemDependency(dependencyCacheKey);
We could make these calls in all of our Controller
classes...
We would need to ensure that each piece of data from the CMS, which a given action is dependent on, is represented as a string dependencyCacheKey
and passed to HttpContext.Response.AddCacheItemDependency()
.
That's not very SOLID ๐, as it violates the following:
- โ Single Reponsibility: Our
Controller
now does route <-> View gluing, and cache management - โ Open/Closed: We can't modify how cache dependency key generation is done for Output Caching without modifying the
Controller
class - โ Dependency Inversion: Our
Controller
now has a dependency onHttpContext
andCacheHelper
, and these are going to be difficult, it not impossible, to test.
SOLID-ifying Our Approach
Kentico's Dancing Goat sample site handles this problem somewhat...
It defines a IOutputCacheDependencies
type that abstracts away the details of output cache management. Good ๐!
But it still injects this interface into every Controller. Bad ๐ฃ!
When we use a type (interface or class) in the same part of our architecture across the entire application, and that type doesn't supply data that is needed to make business decisions, it's very likely that we have a Cross-Cutting Concern on our hands ๐ง!
IOutputCacheDependencies
exists to help with caching, and caching, like logging, is most definitely a cross-cutting concern.
There are several ways to attack these scenarios:
-
Ambient Context: Examples like
HttpContext
andstatic
logging classes -
Inject dependencies everywhere: This is how the Dancing Goat site handles
IOutputCacheDependencies
- Aspect Oriented Programming: Use Decoration across interfaces to centralize the operation
Option 1 results in a lot of repetition, so Don't Repeat Yourself (DRY) is missing and we have a Code Smell. It also violates all the SOLID principles we just listed (Single Responsibility, Open/Close, Dependency Inversion) ๐.
Option 2 fixes the Dependency Inversion violation, but it doesn't solve Single Responsibility and Open/Closed... it's also not DRY ๐.
The third option (my favorite ๐ฅฐ), adheres to Single Responsibility, Open/Closed, Dependency Inversion, and it's as DRY as The Sahara ๐ซ๐ซ๐ซ.
As we will see, it also solves our duplicated management of cache dependency keys between our output caching and query caching ๐๐ฅณ.
The Solution: Combining Query and Output Cache Management
IOutputCacheDependencies
First, I like the idea of the IOutputCacheDependencies
type used in the Dancing Goat code base. Let's use it, but also simplify it:
public interface IOutputCacheDependencies
{
void AddDependencyOnKeys(params string[] cacheKeys);
}
And here's an example implementation:
public class OutputCacheDependencies : IOutputCacheDependencies
{
private readonly IHttpContextBaseAccessor httpContextAccessor;
private readonly ICacheHelper cacheHelper;
private readonly HashSet<string> dependencyCacheKeys;
public OutputCacheDependencies(
IHttpContextBaseAccessor httpContextAccessor,
ICacheHelper cacheHelper)
{
this.httpContextAccessor = httpContextAccessor;
this.cacheHelper = cacheHelper;
dependencyCacheKeys = new HashSet<string>();
}
public void AddDependencyOnKeys(params string[] cacheKeys)
{
foreach (string key in cacheKeys)
{
string lowerKey = key.ToLowerInvariant();
if (dependencyCacheKeys.Contains(lowerKey))
{
return;
}
dependencyCacheKeys.Add(lowerKey);
cacheHelper.EnsureDummyKey(lowerKey);
httpContextAccessor
.HttpContextBase
.Response
.AddCacheItemDependency(lowerKey);
}
}
}
You might that I've also created interfaces for
HttpContext
(IHttpContextBaseAccessor
) andCacheHelper
(ICacheHelper
). I like pushing the un-testable dependencies as far to the outside of my application as possible.This follows the Onion Architecture pattern and allows more of our business logic to be easily testable ๐.
So, now we have a class that lets us define the cache dependency keys of a specific page in our output cache.
If any of these keys are touched due to changes to that content in the CMS, our output cache will be cleared for the pages depending on those keys.
QueryHandlerCacheDecorator
Maybe you already see what's coming... ๐
We already have a central point where all caching is taking place - QueryHandlerCacheDecorator
. It's also the spot where all cache dependency keys for a request to our application are being generated ๐ฎ.
Those keys are what we want to pass to our IOutputCacheDependencies
, so let's wire it all up by passing the IOutputCacheDependencies
to our QueryHandlerCacheDecorator
as a dependency.
First, we update the constructor parameters:
public class QueryHandlerCacheDecorator<TQuery, TResponse>
: IQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
private readonly ICacheHelper cacheHelper;
private readonly IQueryHandler<TQuery, TResponse> handler;
private readonly IQueryCacheKeysCreator<TQuery, TResponse> cacheKeysCreator;
private readonly IOutputCacheDependencies outputCache;
public QueryHandlerCacheDecorator(
ICacheHelper cacheHelper,
IQueryHandler<TQuery, TResponse> handler,
IQueryCacheKeysCreator<TQuery, TResponse> cacheKeysCreator,
IOutputCacheDependencies outputCache)
{
this.cacheHelper = cacheHelper;
this.handler = handler;
this.cacheKeysCreator = cacheKeysCreator;
this.outputCache = outputCache;
}
Then we modify the Execute()
method to pass the generated cacheKeys
, for the given query, to both outputCache.AddDependencyOnKeys()
and cacheHelper.GetCacheDependency()
:
public TResponse Execute(TQuery query) =>
cacheHelper.Cache(
(cacheSettings) =>
{
TResponse result = handler.Execute(query);
if (!cs.Cached)
{
return result;
}
cs.GetCacheDependency = () =>
{
string[] cacheKeys = cacheKeysCreator
.DependencyKeys(query, result);
outputCache.AddDependencyOnKeys(cacheKeys);
return cacheHelper.GetCacheDependency(cacheKeys);
};
return result;
},
new CacheSettings(
cacheMinutes: 10,
useSlidingExpiration: true,
cacheItemNameParts: cacheKeysCreator.ItemNameParts(query)));
}
Bingo, banjo ๐ช!
We now are guaranteed that any cache dependency keys for query caching will also be associated with the output cache of the page.
It's easy to assume that all Kentico data will come from an IQueryHandler
implementation, which means that when we wire up all the pieces, we are safe from missing, typo'd, or mis-configured keys in only one of our caching layers ๐
.
When we mess up our cache keys, at least they'll be consistent ๐คฃ!
And, if we're really worried about these keys, then unit tests on our IQueryCacheKeysCreator
implementations or integration tests on our whole caching layer will validate the quality of our code ๐.
Wrap Up
Caching has always been an important part of Kentico EMS Portal Engine applications, and it's no less important in Kentico 12 MVC.
We typically have 2 caching layers - one for data, and one for HTML output.
Most, if not all, of the data we work with in Kentico will come from the database shared with the CMS, but even if it doesn't, by coming up with a common, segregated interface, like IQueryHandler
, we can use AOP and Decoration to ensure our data caching is always being leveraged โก.
Since Output Caching occurs at a different layer (MVC), it might seem like a difficult task to leverage the cross-cutting caching we already have applied.
However, we've seen how an application that abides by the SOLID principles is both composable and flexible ๐ช.
The same cache dependency keys we generate for data caching can be immediately passed to our output cache management implementation to ensure consistency in features and functionality in regards to caching.
I love it when a plan comes together ๐ค!
As always, thanks for reading ๐!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico blog series:
Posted on November 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 4, 2019