Output caching in ASP.NET Web API

Β· 1146 words Β· 6 minutes to read

Today we will continue with our favorite topic - ASP.NET Web API. I’ve heard folks asking about how you could easily cache the output of the API methods. Well, in ASP.NET MVC, that’s dead easy, just decorate the Action with [OutputCache] attribute and that’s it. Unfortunately, ASP.NET Web API doesn’t have a built-in support for this attribute.

Which doesn’t mean you can’t have it. Let’s build one

Important update - January 2013 & November 2013

This article is outdated - I have since released a Web API CacheOutput, a Web API caching library available on GitHub. Follow this link to learn more and download. .

Alternatively, just use Nuget:

For Web API 2 (.NET 4.5) use:

Install-Package Strathweb.CacheOutput.WebApi2  

For Web API 1 (.NET 4.0), you can still use the old one:

Install-Package Strathweb.CacheOutput  

What are we going to do πŸ”—

We’ll build a class derived from ActionFilterAttribute, which will be responsible for:

  • respond with an HttpResponseMessage built using data from Memory cache (preventing the data retrieval via Controller’s action body)
  • if no data in Memory cache exists, saving there the data returned by the Controller’s Action body
  • adding Cache Control directives, to govern caching on the client side
  • allow caching to be toggled on and off for authenticated users
  • cache only GET requests (purposly excluding other)

Coding - basic stuff πŸ”—

As usually, we start off with MVC4 project > WebAPI template. I recommend using either the latest source from Codeplex or the nightlies from NuGet (this project was built against the builds from 9th May).

So once you have all the latest stuff in GAC (sigh), we can proceed to coding. Let’s add class WebApiOutputCacheAttribute.cs and go from there.

We need to inherit from ActionFilterAttribute:

public class WebApiOutputCacheAttribute : ActionFilterAttribute  
{  
}  

Let’s add a few private properties, that will be useful. The comments explain what they are for.

// cache length in seconds  
private int _timespan;  
// client cache length in seconds  
private int _clientTimeSpan;  
// cache for anonymous users only?  
private bool _anonymousOnly;  
// cache key  
private string _cachekey;  
// cache repository  
private static readonly ObjectCache WebApiCache = MemoryCache.Default;  

Three of them we will set via the constructor

public WebApiOutputCacheAttribute(int timespan, int clientTimeSpan, bool anonymousOnly)  
{  
_timespan = timespan;  
_clientTimeSpan = clientTimeSpan;  
_anonymousOnly = anonymousOnly;  
}  

Before we proceed to implementing the caching itself, let’s add two private helpers. First we will recognize whether or not the request should be cached

private bool _isCacheable(HttpActionContext ac)  
{  
if (\_timespan > 0 && \_clientTimeSpan > 0)  
{  
if (_anonymousOnly)  
if (Thread.CurrentPrincipal.Identity.IsAuthenticated)  
return false;  
if (ac.Request.Method == HttpMethod.Get) return true;  
}  
else  
{  
throw new InvalidOperationException("Wrong Arguments");  
}  
return false;  
}  

So in this case we check if both of the timespan variables have positive values, whether we should cache for authenticated users, and finally if we are dealing with a GET request.

The second private helper is below:

private CacheControlHeaderValue setClientCache() {  
var cachecontrol = new CacheControlHeaderValue();  
cachecontrol.MaxAge = TimeSpan.FromSeconds(_clientTimeSpan);  
cachecontrol.MustRevalidate = true;  
return cachecontrol;  
}  

This is how we set the client side caching values, using Cache-Control in the Headers. We wrap this functionality into a separate method, since we’ll call it from two different places. More on that to come.

Coding - OnActionExecuting πŸ”—

Next step is to override OnActionExecuting base method. This is the method that executes prior to hitting the Controller’s action. This is precisely the place where we can:

  1. check if there is some data in the cache, and if all caching conditions are met
    2a. if so, return the appropriate HttpResponseMessage to the client
    2b. if not, continue to the Controller a grab the data from where it should be coming from
public override void OnActionExecuting(HttpActionContext ac)  
{  
if (ac != null)  
{  
if (_isCacheable(ac))  
{  
_cachekey = string.Join(":", new string[] { ac.Request.RequestUri.AbsolutePath, ac.Request.Headers.Accept.FirstOrDefault().ToString() });  
if (WebApiCache.Contains(_cachekey))  
{  
var val = (string)WebApiCache.Get(_cachekey);  
if (val != null)  
{  
ac.Response = ac.Request.CreateResponse();  
ac.Response.Content = new StringContent(val);  
var contenttype = (MediaTypeHeaderValue)WebApiCache.Get(_cachekey + ":response-ct");  
if (contenttype == null)  
contenttype = new MediaTypeHeaderValue(_cachekey.Split(':')[1]);  
ac.Response.Content.Headers.ContentType = contenttype;  
ac.Response.Headers.CacheControl = setClientCache();  
return;  
}  
}  
}  
}  
else  
{  
throw new ArgumentNullException("actionContext");  
}  
}  

If the request passes our caching rules, we build up a cache key. That has a format of [RequestUri.AbsolutePath:Request Content Type]. This way we will not accidentatlly serve a Response with wrong content type. Also, we will separately cache requests for individual objects i.e. Get(int id) and all such.

We will also need to store/retrieve the Response Content Type in/from the cache. that is because the Request and Response content types may not always be the same - for example by default (without injecting any custom formatters) if you request an ASP.NET Web API from the browser - “text/html” the response will come as “application/xml - so we don’t want to ruin that.

We check the Memory cache for the object (string, since the response content is just string), and if it’s there we we also try to get the response content type from the cache (as a fallback, we use Request Content Type). The key for retrieving the CT uses similar format - [RequestUri.AbsolutePath:Request Content Type:response-ct. Then we need to create a new Response instance, set the Content Type correctly and set the client cache directives according to what’s expected. Then we exit the method and this means the Controller’s action body never even executes.

Coding - OnActionExecuted πŸ”—

The final piece to our outputcache puzzle is overriding OnActionExecuted base method. This is invoked, as the name suggests, upon the completion of execution of the Controller’s action (method). We need to handle this event for situations in which we wish to populate the cache with some data (our code from above is only relevant when there is something in cache already).

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
if (!(WebApiCache.Contains(_cachekey)))  
{  
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;  
WebApiCache.Add(\_cachekey, body, DateTime.Now.AddSeconds(\_timespan));  
WebApiCache.Add(\_cachekey+":response-ct", actionExecutedContext.Response.Content.Headers.ContentType, DateTime.Now.AddSeconds(\_timespan));  
}  
if (_isCacheable(actionExecutedContext.ActionContext))  
actionExecutedContext.ActionContext.Response.Headers.CacheControl = setClientCache();  
}  

We check if there isnt really anything in the cache using our cachekey - if not, we add both the Response body and the Response content type to the cache, using the key structure we agreed upon before. Finally, we set the client cache directives anyway, since they are independent from Memory caching and should be always part of our Response.

Usage πŸ”—

Now we can easily use our custom attribute inside our ApiControllers.

// GET /api/values  
[WebApiOutputCache(120, 60, false)]  
public IEnumerable<string> Get()  
{  
return new string[] { "value1", "value2" };  
}

// GET /api/values/5  
[WebApiOutputCache(120, 60, false)]  
public string Get(int id)  
{  
return "value";  
}  

We can test this in Fiddler/browser (for client side caching) or VS debugger (for Memory caching) to see that indeed we are getting the cached response.

Source πŸ”—

As always, the source files are available for download, just like last time via gitHub.There is no point in including the entire solution, so I provide just the WebApiOutputCache.cs. Till next time my friends!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP