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 Articles 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
Controllerclass for demonstration only.Controllerclasses 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: IQuerydefines data,IQueryHandlerdefines implementation
- โ Open/Closed: Easy to apply Aspect Oriented Programming through Decoration
- โ
 Liskov Substitution: We can supply any implementation of our IQueryHandlerto the aboveArticleController
- โ
 Interface Segregation: IQueryHandlerhas 1 method:Execute()
- โ
 Dependency Inversion: Interfaces, like IQueryHandlerallow 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
ISiteContextandICultureContextas 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 Controllernow 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 Controllerclass
- โ Dependency Inversion: Our Controllernow has a dependency onHttpContextandCacheHelper, 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 HttpContextandstaticlogging 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:
 
 
              
 
    
Top comments (0)