Output caching in ASP.NET Web API

Strath

Output caching in ASP.NET Web API

Because you can write your own OutputCacheAttribute

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

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.

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:

C#
1
2
3
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
}

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

C#
1
2
3
4
5
6
7
8
9
10
// 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

C#
1
2
3
4
5
6
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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

C#
1
2
3
4
5
6
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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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).

C#
1
2
3
4
5
6
7
8
9
10
11
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.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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!
source on github

  • manning

    Exactly what I was missing. This (or something of this kind) should be part of the core!!!

  • Pingback: Dew Drop – May 11, 2012 (#1,325) | Alvin Ashcraft's Morning Dew

  • Ethan

    I somehow did not get this working first was
    that ac.Response = ac.Request.CreateResponse(); getting an error on CreateResponse method and the other is that there is no extension or method for var body = actionExecutedContext.Response i.e There is only Request and no Response

    • Filip W

      Hi Ethan,

      which Web API build are you using? It seems as if the Beta, which is obsolete – because both the method (HttpRequestMessageExtensions.CreateResponse) and the property (HttpActionExecutedContext.Response) are part of the new build, see here and here.Please try running the code against the latest source from Codeplex, or using the nightly builds from Nuget. It might also be that some of your DLLs in GAC are still the old ones. For more instructions on working with nightly builds see this excellent post by Henrik.

      cheers
      /f

  • Chad

    Have you considered submitting a pull request for this?

  • Pingback: Distributed Weekly 155 — Scott Banwart's Blog

  • scwd

    Very informative blog.Much thanks again.

  • Cory

    That worked fantastic for me, thanks for putting that together!

  • Rob

    Hello,

    I recently rolled out a similar implementation based on your post above, and I’m currently experiencing cache mismatch issues (like the wrong things are getting sent back)

    Trying to dig into the issue I found this:
    http://msdn.microsoft.com/en-us/library/system.web.mvc.actionfilterattribute(v=vs.108).aspx

    “Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe”

    Any thoughts?

  • Steven SUing

    Do you have an example of Client Side Caching using HttpClient?

  • API_Caching_Newbie

    Hi,

    This is exactly what i need and it works great . I just have one hick up if the data i requested is cached and someone updated one or more of the records in that data set and i make another request i get the cached values. I was wondering if there is a way around this?

  • http://www.cleytonferrari.com Cleyton Ferrari

    Hello, worked perfect!
    Only, if you have a request parameter as in Get (id), it always returns the same, I believe that this line of code is the problem:

    _cachekey = string.Join(“:”, new string[] { ac.Request.RequestUri.AbsolutePath, ac.Request.Headers.Accept.FirstOrDefault().ToString() });

    Just not sure what change there for him to return values ​​according to the ID passed.

  • http://www.cleytonferrari.com Cleyton Ferrari

    Hello, I managed! to cache for each request with different parameters, it was enough to change the following code:

    ac.Request.RequestUri.AbsolutePath
    changed by
    ac.Request.RequestUri.AbsoluteUri

    Thank your code helped a lot! this should be part of the API! thank you!

    • Filip W

      Thanks! Yeah I didn’t update the post, but if you look in the github repo a while ago it was updated with the exact same change you suggested :) TO avoid problems with querystring

  • Pingback: ASP.NET Web API CacheOutput « Alexandre Brisebois

  • Tohid

    Filip,

    As far as I know, RESTful web services (like ASP.NET Web API) are treated similar to any HTTP (or HTTPS) request/response by IIS.

    While we turn on IIS Output Caching on our production server, is it really useful to implement such an in-code caching mechanism?

    PS: Thanks for your awesome posts. I’m a big fan of your blog.

  • http://www.aboutmycode.com/ Giorgi Dalakishvili

    There is a bug in OnActionExecuting:

    If the item is found in cache the code does not set CacheControl so it defaults to “Cache-Control:no-cache”. As a result client side caching is not used any more.

  • http://twitter.com/JonnyGibson Jonny G

    This is great! is there anyway to refresh the cache in the background? so, if i have a 30 sec cache on a slow method, every 30 secs one user will connect and it will be slow for him, as he’ll not get the cached response.

  • Aaron

    Am I missing something, or does it seem like a better approach might be to simply check the cache-control headers of the request, and return a 304-not modified in the event that the content is still current? Then you wouldn’t have to mess around with caching any of these objects in memory.

    I appreciate your thoughts on this approach or anything that I may have missed from your post.

    • http://twitter.com/darrel_miller Darrel Miller

      You can only return a 304 if the client makes a conditional GET using If-modified-since or If-non-match. Most clients don’t do this, so you actually do need to return the response.

  • http://twitter.com/darrel_miller Darrel Miller

    I ran into a minor issue. If the client does not provide an accept header, then creating the CacheKey fails. Also, the cachekey is based on the first media type in the accept string. Shouldn’t it really be based on the server negotiated media type?

    • http://www.strathweb.com/ Filip W

      That’s a great suggestion. Working on a batch of changes now, with many many changes.

      But this we pull off using actionDescriptor and manual conneg:

      //set default to accept or at least json
      _responseMediaType = actionContext.Request.Headers.Accept != null ? actionContext.Request.Headers.Accept.FirstOrDefault() : new MediaTypeHeaderValue(“application/json”);

      var config = actionContext.Request.GetConfiguration();
      var negotiator = config.Services.GetService(typeof (IContentNegotiator)) as IContentNegotiator;

      if (negotiator != null)
      {
      var negotiatedResult = negotiator.Negotiate(actionContext.ActionDescriptor.ReturnType, actionContext.Request, config.Formatters);

      //if we successfully negotiated, override default
      _responseMediaType = negotiatedResult.MediaType;
      }

  • Anders

    Hello, I just saw that you have created a GitHub link, but here goes anyway.

    I used the code above and ran into a big problem. If the server gets a lot of hits at the same time the outputcache starts returning the wrong values. Here is a simple scenario:

    ApiController:

    public class ReturnValueController : ApiController
    {
    [WebApiOutputCache(120, 60)]
    public int Get(int id)
    {
    return id;
    }
    }

    And loadtester, just a console app:

    class Program
    {
    private static void Main(string[] args)
    {
    var errors = 0;
    var total = 0;
    for (var i = 0; i < 10; i++)
    {
    var tasks = new List<Task>();
    for (var j = 0; j < 20; j++)
    {
    tasks.Add(Download(j));
    }

    Task.WhenAll(tasks.ToArray()).Wait();

    foreach (var task in tasks)
    {
    total++;
    if (task.Result)
    errors++;
    }
    }

    Console.WriteLine("Number of errors: " + errors + " / " + total);
    Console.ReadKey();

    }

    static async Task Download(int idx)
    {
    using (var wc = new WebClient())
    {
    wc.Headers.Add(“Accept”, “application/json”);
    var url = “http://localhost:13231/api/ReturnValue/” + idx;
    var result = await wc.DownloadStringTaskAsync(url);

    var expected = idx;
    var error = !result.Contains(expected.ToString());

    return error;
    }
    }
    }

    You can try this scenario on your new build and see if it still fails…

    • http://www.strathweb.com/ Filip W

      don’t use this code any more it’s deprecated, use the library from GitHub. There are no locks on the cache add here

  • Sean McGettrick

    Maybe I’m missing it in your GitHub readme, but when an item expires and is removed from the cache, where can you specify a callback to handle that event?

  • coryisakson

    Thank you for the project and the NuGet installer! I was able to implement the Client caching I wanted in just a few seconds. And having the server caching via the same attribute was a nice bonus!