When building highly performant web applications, it’s always important to consider your caching strategy. There are some generic things you can do to make your overall website faster (such as setting efficient client-side cache policies), but often times a much overlooked performance pitfall involves making too many API calls. Luckily, Episerver’s Object Caching can help you avoid this trap.
Caching Expensive Operations
Imagine a scenario where you need to look up data from a back-office system (products, prices, locations, reviews… whatever). The data is exposed as a REST endpoint, and accepts a number of query parameters to retrieve various slices of data. This is quite common in web architecture, and is one of the pinnacles of a modern and rich web experience. Often times we integrate our systems together with “service layer glue” to avoid monolithic architectures, but if we do this wrong we can introduce massive performance impacts.
Let’s take the reviews example. Suppose I have a product details page, and reviews are displayed at the bottom of this page below-the-fold. Every time the product detail page loads, reviews need to be fetched and loaded as well. These reviews could be ajax’d in or loaded on initial page load- the outcome is still the same: the page isn’t fully loaded until the reviews have loaded.
For instance, you may have a simple service like so:
interface IReviewService { List<Review> FetchReviewsFor(string productId); }
Now, the implementation of this service probably involves an expensive operation. It may be using HttpClient or RestSharp to fetch reviews from an API, they could be fetched from a database, etc. The actual implementation doesn’t matter, so I’ve mocked it up for demonstration purposes:
List<Review> FetchReviewsFor(string productId) { List<Review> retVal = new List<Review>(); retVal = ...; // logic here to read from external system return retVal; }
The problem is that this operation may take some time to execute, and this execution may be blocking the calling thread. Sure, we could implement this as async, but that doesn’t affect your actual page speed if this is called at page rendering time. On top of this, our implementation is not very fault tolerant, as outages in the underlying review storage system will trigger outages for anyone calling this method. This is where a caching layer really becomes beneficial.
The Target Implementation
Imagine if we could write the above code like this:
List<Review> FetchReviewsFor(string productId) { var cacheKey = $"reviews-for-{productId}"; List<Review> retVal = _cacheService.Get<List<Review>>(cacheKey); if (retVal == null) { retVal = ...; // logic here to read from external system _cacheService.Insert(cacheKey, retVal); } return retVal; }
In this updated logic, we first attempt to fetch a list of review objects from our in-memory cache. If we find them, then we return them from the cache. If we don’t find them, then we fetch them from the source and cache the results for the next caller. There’s an implicit assumption here that we don’t need review data to be always up to date, and that eventually the cache will “clear itself out”. So how do we get ahold of this special _cacheService
object?
To achieve this, we can create a facade around Epi’s native IObjectInstanceCache
and inject that wrapper as a dependency into our services. A good starting point implementation looks like this:
public class CacheService : ICacheService { private IObjectInstanceCache _cache; private const string MASTER_KEY = "mysite-master-key"; public CacheManager(IObjectInstanceCache cache) { _cache = cache; } public T Get<T>(string key) where T : class { return _cache.Get<T>(key, ReadStrategy.Immediate); } public CacheEvictionPolicy GetDefaultPolicy() { int minutes = 5; return new CacheEvictionPolicy(new TimeSpan(0, minutes, 0), CacheTimeoutType.Absolute, Enumerable.Empty<string>(), new List<string>() { MASTER_KEY }); } public void Insert(string key, object value) { var cachePolicy = GetDefaultPolicy(); Insert(key, value, cachePolicy); } public void Clear(string key) { _cache.Remove(key); } public void ClearAll() { _cache.Remove(MASTER_KEY); } }
The important thing to note here, is that GetDefaultPolicy()
is currently hard-coded to cache results for up to 5 minutes. This is a setting that you could expose within the CMS or push to a configuration file. You may want this to be aggressively small (such as 5 minutes or less), or ramped up to be very conservative, such as 2 or more hours. The other thing to note is that I am tying this caching policy to all insertions and all cached objects are tied to the same master key. Feel free to swap this out as you see fit. You may also invoke the Clear()
or ClearAll()
operations upon specific actions, such as a content publish, or as a manual tool for site administrators when debugging issues.
This is a common strategy that I have used on every project in the past few years, regardless of whether the platform is Episerver or otherwise. I find this to be highly effective, and putting the right strategy in place really does set you up for being that much faster when it comes to implementing modern web solutions.
Enjoy!
Hi Dylan, Great post!! I recommend for Episerver you use ISynchronizedObjectInstanceCache instead of IObjectInstanceCache. This ensures the cache is invalidated across all instances on a web farm. i.e. DXP