In the past, I have always used the OutputCacheAttribute
when I wanted to cache the result of an Action in my MVC application; it’s simple, and it gets basic caching up and running with very little effort.
Unfortunately “simple and quick” are not quite as useful when you need a little more control…
Scenario
Let’s say you have a resource that you automatically generate in your controller, but that creating that resource takes a long time…
public async Task<ActionResult> Slow()
{
var resource = await _service.GenerateResource();
// ...5 minutes later...
return View(resource);
}
Understandably, you want to cache the result of this action so that subsequent visitors don’t all have to wait around for the resource to be created. [OutputCache]
to the rescue, right?
Not So Fast!
Unfortuantely, the resource is not entirely constant – it can be altered by certain user actions in other areas of the application, and when that happens it will need to be regenerated.
[OutputCache]
will allow us to vary the cached result by a number of variables (host, action arguments, custom strings…) and supports timeouts etc, but it does not allow another piece of code somewhere in the application to say “that resource is not longer valid”.
An Alternative to [OutputCache]
One alternative means of caching the results of an action is to call the AddCacheDependency
method on the Response
object:
public async Task<ActionResult> Index()
{
Response.AddCacheDependency(new CacheDependency(...));
//...
}
The CacheDependency
instance in this example is an ASP.NET class that is used by the caching framework to determine when the resource we created has been changed.
The base CacheDependency
implementation allows you to specify one or more file system paths that will be monitored, and will invalidate itself when any of those files is updated. There is also a SqlCacheDependency
class that observes the results of a SQL query and invalidates when they change.
Neither of these implementations will give us quite what we are looking for - the ability to invoke a method from anywhere within the codebase that explicitly marks the cached content as changed – but they are leading us in the right direction.
If we can create a CacheDependency
instance that is uniquely identifiable for the slow resource then we can add it to the response and force it to invalidate itself at a later date.
Extending CacheDependency
Before we can get into how we add our CacheDependency
instance, we need to create an implementation that will allow us to explicitly invalidate it through code. Thankfully, CacheDependency
exposes a number of methods to it’s inheriting classes that mean we can achieve our explicit invalidate very easily.
The minimum that we need to do to make the CacheDependency
work is to pass a list of paths into the base constructor and to provide a valid value from the GetUniqueID
method. We know that we do not want to depend on any file system resources so we can just pass an empty list into the constructor, and as we need a unique ID anyway (to identify the cached resource later) we can just pass this into the constructor.
class ExplicitCacheDependency : CacheDependency
{
private string _uniqueId;
public ExplicitCacheDependency(string uniqueId)
: base(new string[0]) //no file system dependencies
{
_uniqueId = uniqueId;
}
public override string GetUniqueID()
{
return _uniqueId;
}
}
CacheDependency
has a protected NotifyDependencyChanged
method that will notify the caching framework that the cached item is no longer valid. In most implementations this would be invoked in some callback, but for our purposes we can just add a new Invalidate
method and invoke it directly:
public void Invalidate()
{
base.NotifyDependencyChanged(this, EventArgs.Empty);
}
Voila – a cache dependency that we can explicitly invalidate. But how can we get a reference to this in the various places that we need it?
CacheDependencyManager
Creating a new cache dependency doesn’t help us much if we can’t get a reference to it later - otherwise, how can we call Invalidate
? Let’s create a new class that handles the creation and invalidation of the new cache dependencies: CacheDependencyManager
.
class CacheDependencyManager
{
private Dictionary<string, ExplicitCacheDependency> _dependencies
= new Dictionary<string, ExplicitCacheDependency>();
public CacheDependency GetCacheDependency(string key)
{
if (!_dependencies.ContainsKey(key))
_dependencies.Add(key, new ExplicitCacheDependency(key));
return _dependencies[key];
}
public void InvalidateDependency(string key)
{
if (_dependencies.ContainsKey(key))
{
var dependency = _dependencies[key];
dependency.Invalidate();
dependency.Dispose();
_dependencies.Remove(key);
}
}
}
Note: in the interests of brevity I have not included the thread-safe version here; this is a terrible idea in the real world, so make sure you include some appropriate locking!
This CacheDependencyManager
is intended to hide the detail of how the dependency instances are created and invalidated from calling classes, which it achieves through 2 public methods:
GetCacheDependency
that creates a new ExplicitCacheDependency if none exists for the specified key, or returns the cached one if it has been previously createdInvalidateDependency
that attempts to locate an existing cache dependency for the specified key, and invalidates and removes it if one is found. If one doesn’t exist then it does nothing, so callers don’t need to know whether or not the resource has already been cached
One quick singleton pattern later and we can call these methods from throughout our codebase. When we invoke our slow action for the first time we need to add the dependency to the response using a unique key to identify the slow resource:
Response.AddCacheDependency(
CacheDependencyManager.Instance.GetCacheDependency("ResourceKey"));
And how do we invalidate the resource after some arbitrary user action? We can just pass that same key into the Invalidate
:
public class SomeOtherController : Controller
{
public ActionResult SomeRandomUserAction()
{
CacheDependencyManager.Instance.Invalidate("ResourceKey")
//...
}
}
Obviously you could (should!) use dependency injection to pass the CacheDependencyManager
to your controllers instead of accessing it directly, but the singleton will suffice for this example.
That’s All Folks
That’s the lot - now we can manually invalidate our slow resource whenever and from wherever we want, and our users can enjoy speedy cached resources the rest of the time!