DEV Community

Cover image for Kentico 12: Design Patterns Part 17 - Centralized Cache Management through Decoration
Sean G. Wright
Sean G. Wright

Posted on

Kentico 12: Design Patterns Part 17 - Centralized Cache Management through Decoration

Photo by Alisa Anton on Unsplash

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 ๐Ÿง:

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);
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 above ArticleController
  • โœ… 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);
}
Enter fullscreen mode Exit fullscreen mode

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
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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 and ICultureContext as dependencies instead of passing that data through the ArticlesQuery, 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() 
        };
}
Enter fullscreen mode Exit fullscreen mode

If you find yourself generating a lot of cache dependency keys by hand, check out my FluentCacheKeys NuGet package โšก:


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)));
}
Enter fullscreen mode Exit fullscreen mode

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<,>));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 on HttpContext and CacheHelper, 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:

  1. Ambient Context: Examples like HttpContext and static logging classes
  2. Inject dependencies everywhere: This is how the Dancing Goat site handles IOutputCacheDependencies
  3. 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You might that I've also created interfaces for HttpContext (IHttpContextBaseAccessor) and CacheHelper (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;
    }
Enter fullscreen mode Exit fullscreen mode

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)));
}
Enter fullscreen mode Exit fullscreen mode

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:

#kentico

Or my Kentico blog series:

Top comments (0)