September 10, 2011

Managing cache dependencies with CacheEntryChangeMonitor

The caching facilities in System.Runtime.Caching are a vast improvement over what we had prior to .NET 4. Unfortunately many of the new features are not very well documented, including how to use the CacheEntryChangeMonitor.

If you use caching extensively in your application you may find you are writing a lot of code to manage cache dependencies.

Take the following code:

public IEnumerable<Product> GetProducts() {
    // from cache "products"
}

public IEnumerable<FeaturedProduct> GetFeaturedProducts() {
    // from cache "featuredproducts"
}

“GetFeaturedProducts” makes a call to “GetProducts”, does some processing and then caches the collection (we don’t want to perform this processing every time).

As “GetProducts” is also using the cache, we need to ensure that if a product is added or updated, both collections are removed from the cache.

One way of doing this is to invalidate each cached item explicitly:

public void AddProduct(string name, bool isFeatured) {
    var p = new Product(name, isFeatured);
    db.Save(p);
	
    cache.Remove("products");
    cache.Remove("featuredproducts");
}

However, you can see that this may become difficult to maintain, especially as you’ll need to do this in your Update/Delete methods too.

Fortunately we can make use of the CacheEntryChangeMonitor to handle such dependencies. From MSDN:

The CacheEntryChangeMonitor is used when a cache implementation has to monitor changes to entries in its own cache.

To demonstrate this I’ve created a simple MVC web application that caches the current date for 1 minute and outputs this to the page.

public ActionResult Index()
{
    var obj = GetFromCache("datetime", () => DateTime.Now, 1, "cachebreaker");
    return Content(obj.ToString(), "text/plain");
}

This item has a dependency on another item in the cache: “cachebreaker”. The idea is that we can then simply update our cache breaker to remove all dependent items from the cache:

private void InvalidateCache() {
    cache.Set("cachebreaker", Guid.NewGuid(), DateTimeOffset.Now.AddHours(1));
}

To make sure that our cache breaker exists prior to caching the date, we add the following to Application_Start in global.asax:

MemoryCache.Default.Set("cachebreaker", 
        Guid.NewGuid(), 
        DateTimeOffset.Now.AddHours(1));

To test this out, I added another Action method to our controller that updates the cache breaker:

public ActionResult ClearCache() {

    InvalidateCache();

    return RedirectToAction("Index");
}

When we run our application for the first time the current date time will be displayed. The same date time will be displayed for a minute, until our cache expires.

However, we can invalidate our cache at any time by making a request to our ClearCache action. You will then see that the date time has been updated (as the cached date time was invalidated).

Finally the cache helper methods that do most of the work:

private object GetFromCache(
    string key, 
    Func<object> aquire, 
    int minutes, 
    params string[] dependencies) 
{
    if (cache.Contains(key)) // here cache is MemoryCache.Default
    {
        return cache.Get(key);
    }

    var result = aquire();
    AddToCache(key, result, minutes, dependencies);

    return result;
}

private void AddToCache(
    string key, 
    object value, 
    int minutes, 
    string[] dependencies)
{
    var policy = new CacheItemPolicy {
        AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(minutes)
    };

    if (dependencies != null && dependencies.Count() > 0) {
        policy.ChangeMonitors.Add(
            MemoryCache.Default.CreateCacheEntryChangeMonitor(dependencies)
        );
    }

    cache.Add(key, value, policy);
}

© 2020 Ben Foster