Skip to main content

The new 37signals new basecamp is designed to be really fast. One of the ways it accomplished it is through key based caching.

---
title: Key Based Cache in ASP.NET
author: Alan D. Jackson
date: April 17, 2012
source: https://alandjackson.wordpress.com/2012/04/17/key-based-cache-in-mvc3-5/
---

The new 37signals new basecamp is designed to be really fast.  One of the ways
it accomplished it is through key based caching.  I am not going to re-hash what
they already explained, so if you aren't familiar with it, please go and read
these two posts:

- [How key-based cache expiration works](http://37signals.com/svn/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui)
- [How Basecamp Next got to be so damn fast without using much client-side UI](http://37signals.com/svn/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui)

I don't work on applications with nearly their scale, but who doesn't like fast
applications? My environment happens to be ASP.NET MVC3 not Ruby on Rails, but
the implementation of a key based cache is very simple so it was no problem
getting it up and running.

There are really only two parts to this extremely simple system: the cache to
store the data, and the controllers that use the cache.

## 1. The Cache

My applications are small enough that I am going to use the http runtime cache.
This is only going to work for the smallest of applications.  If you are running
multiple servers or need a ton of memory, this isn't going to work (look at
something like Redis).

In order to prevent the cache from expiring and from taking up too much memory,
I set it up in the web.config like this (set to never expire and use a max of 1GB of memory):

```xml
<system.web>
  <caching>
    <cache disableExpiration="true" privateBytesLimit="1073741824"/>
  </caching>
</system.web>
```

A quick and dirty HttpRuntime method extension will do the trick:

```cs
public static class CacheExtensions
{
    public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator) where T : class
    {
        if (ConfigurationManager.AppSettings["DISABLE_CACHE"] == "True")
            return generator() as T;

        T value = cache[key] as T;
        if (value == null)
        {
            value = generator();
            cache.Insert(key, value, null, DateTime.MaxValue, Cache.NoSlidingExpiration);
        }

        return value;
    }
}
```

Used like this:

```cs
HttpRuntime.Cache.GetOrStore<string>(key, () => GenerateContent());
```

If you want to hide the cache implementation details so that later you can move
to something else, you'd probably want to create a cache utility class like
this:

```cs
public class AppCache
{
    public static T FromCache<T>(Func<T> create) where T : class
    {
        return FromCache<T>(typeof(T).FullName, create);
    }

    public static T FromCache<T>(string name, Func<T> create) where T : class
    {
        if (HttpRuntime.Cache[name] == null)
        {
            HttpRuntime.Cache.Insert(name, create(), null, DateTime.MaxValue, Cache.NoSlidingExpiration);
        }

        return (T)HttpRuntime.Cache[name];
    }
}
```

And then used like this:

```cs
AppCache.FromCache(key, () => GenerateContent());
```

## 2. The Controller Methods

The most important part of a key based cache is a good key.  Hopefully you have
records in the database with an updated_at field or similar so the key can be
the results of something like `SELECT MAX(updated_at) FROM my_table WHERE (...)`.

I separate out my cached controller method from the one that does the actual
work.  Also, I need to return the actual html string result rather than a view
so it can be cached.

```cs
public ActionResult Index()
{
    var key = db.Entities.Max(e => e.updated_at).ToString("yyyyMMddhhmmss");
    return Content(AppCache.FromCache<string>("v1/Entities/Index/" + key, () => IndexGenerate()));
}

public string IndexGenerate()
{
    return RenderViewToString(this, "Index", Db.GetItems());
}

public static string RenderViewToString(Controller controller, string viewName, object model)
{
    controller.ViewData.Model = model;
    try
    {
        using (StringWriter sw = new StringWriter())
        {
            ViewEngineResult viewResult = ViewEngines.Engines.FindView(controller.ControllerContext, viewName, "");
            ViewContext viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);

            viewResult.View.Render(viewContext, sw);

            return sw.GetStringBuilder().ToString();
        }
    }
    catch (Exception ex)
    {
        return ex.ToString();
    }
}
```