Clearing Obsolete Cache Entries with Webhooks
Do you store your Kentico Cloud content in your app's cache? Have you ever wanted to keep the cache clutter free, with just up-to-date entries? In this how-to article I'll show how to go about clearing the obsolete cache entries upon a webhook call made from Kentico Cloud.
In a step-by-step manner, I'll configure a webhook in Kentico Cloud and create a simple ASP.NET Core MVC app that listens to webhook calls to clear its cache entries appropriately.
In general, apps that utilize Kentico Cloud don't need much caching. Kentico Cloud Delivery/Preview API service exposes your content through a worldwide-operating CDN network with response latencies measured in milliseconds. So your web apps don't necessarily need to cache the content and can refer to Kentico Cloud directly.
But, there might be a reason for you to implement caching: you may wish to save on API requests made to Kentico Cloud. Our free plan includes 50,000 API requests per month and our paid plans include even higher amounts of prepaid requests, so you don't have to feel compelled to cache your content, but if you wish to do so, here's how you'd set up webhooks to keep the cache up to date.
So you have an app that caches Kentico Cloud content items for a fixed period of time. But what happens when a content contributor alters the content item in Kentico Cloud before the cache entry expires? The app will serve an obsolete version of the content item. Assuming that this is something you don't wish to happen, we've designed the webhooks feature. It's not just for cache invalidation; webhooks cater for various other cases in which apps wish to get signals of events happening in Kentico Cloud. See the article updating an Azure Search index with webhooks by Kentico Technical Evangelist, Bryan Soltis, for an example.
Webhooks are nothing but HTTP POST requests that Kentico Cloud sends to the preconfigured, publicly routable URL addresses of your apps. The content of the webhook request (i.e. the payload) always contains detailed information about what has happened. This way, your app can react appropriately—e.g. clear an obsolete cache item.
Set Up Kentico Cloud
As advertised above, the first place I went to set things up was Kentico Cloud. Here, I went to the main menu and chose "Webhooks". This menu item is accessible to the "Developer" and "Project manager" roles.
Thereafter, the steps were pretty straightforward.
I clicked "Create new Webhook" and was presented with a simple dialog.
I entered "Cache Webhook" as the name and then filled in the public URL of my future app's webhook endpoint ("http://example.com/webhook"). Then, once I had noted down the generated secret, I was ready to create my app.
Create the Example App
If you expected this part to be complicated, I'll disappoint you now: it’s fairly easy.
As before, I deployed our boilerplate code template via the "Dotnet New" command:
From the highest-level perspective, I implemented the logic in the following steps:
- I created a class called CacheManager which is responsible solely for managing the state of the MemoryCache.
- I slightly modified the code of the existing CachedDeliveryClient class so that it just prepares identifiers (codenames) for newly created cache items and its dependencies. It then calls the CacheManager class to work with the cache.
- I added a new MVC controller called WebhookController to represent the webhook endpoint mentioned above. The controller calls the CacheManager to invalidate cache entries.
The CacheManager Class
Apart from holding the reference to the MemoryCache object, this singleton class has two significant methods: GetOrCreateAsync<T> and InvalidateEntry.
The first method is called by the CachedDeliveryClient's GetItem(s)Async methods. The method accepts "identifierTokens", "valueFactory", and "dependencyListFactory" as input parameters.
The "valueFactory" delegate is used to build a fresh cache entry (by calling the Delivery/Preview API endpoint) should the cache entry be expired. The "dependencyListFactory" is then responsible for reading that entry and returning a collection of identifiers of entries that the current entry depends upon. The CachedDeliveryClient has specific methods for both the "valueFactory" and "dependencyListFactory" parameters.
The CreateEntry<T> method invokes the "dependencyListFactory" and uses the identifiers to create dummy cache items of all the dependencies. Each dummy entry holds a CancellationTokenSource object that advertises invalidation of its corresponding cache entry to other entries that subscribe to its cancellation token. This is how dependencies are dealt with in .NET Core.
Finally, the InvalidateEntry method simply invalidates (clears) a specific cache entry, along with all entries that depend upon it. The InvalidateEntry method is called by the WebhookController class.
The CachedDeliveryClient Class
The GetItem(s)Async method now just calls the CacheManager. For instance, the following is the code of the strongly typed overload of the GetItemAsync method.
And here's what the "dependencyListFactory" instance looks like (the GetDependencies<T> method):
The AddModularContentDependencies method and other backend methods simply rely on type dynamic to extract codenames from the ModularContent property.
The WebhookController Class
The job of the controller is simply to:
• check the signature (via the KenticoCloudSignatureActionFilter)
• check the type of the Kentico Cloud artifacts and their codenames
• invoke the InvalidateEntry method, if needed
There were basically two things to test:
- whether the cache gets populated with all its dependencies
- if the call to the WebhookController invalidates the cache entries
The cache was correctly populated, including the dummy entries of all the ModularContent items. In the case of listing responses, all content items in the listing produced their corresponding dummy entries. So, if any of the items in either the modular content or the listing were obsolete, the whole listing would have been properly invalidated.
Upon a WebhookController call, the cached items and listings (i.e. the cache entry) were first marked invalid and then purged. The MemoryCache's Get method correctly returned “null” instead of the stale cache entry. That in turn caused the app to fetch fresh content. The cache entry was also purged when one of the dependencies was invalidated.
All was set and done.
Get the Code
There are a few things that could be improved. E.g., the content items in the cache could be invalidated when their underlying content type is altered in Kentico Cloud. If you wish to see that, just tell us, either through the comments below or in our forums!