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();
}
}
```