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 & 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:

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

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:

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

Three of them we will set via the constructor

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

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:

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

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).

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.

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

Be Sociable, Share!

  • 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!

  • Ehsan

    it was fine, thanks

  • Matthew

    Darrel – Thanks for the great blog post and the Github code. A question for you: We are going to be implementing our code in the web api v2. Is there built-in support for caching in v2? Are there any changes we would need to make to use your caching module in v2?

    BTW – Sorry about the Leafs. Go Hawks :)

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

      no there is no built in caching in Web Api 2, and yes the library works with Web Api 2 as well – in terms of filter handling nothing changes (except Web API 2 supports now filter overrides so I might end up doing something with that to add some extra functionality!)

  • http://indie.by/ massimo_indie

    I have problem:
    Queries can not be applied to a response content of type ‘System.Net.Http.StringContent’. The response content must be an ObjectContent. Parameter name: actionExecutedContext

    Please, help.

  • antao

    Good contribution Filip. Thanks.

  • Bob

    This is nice. I also implemented something similar. My current task involves changing the implementation we both chose, however, so that the OnExecuting logic is as follows:

    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, check to see if a task is currently running to get this information
    2.b.1 If so, wait until that task is complete, then send the information back to the user
    2.c. Continue to the controller method, and cache the result

    Any suggestions?

  • Jonathan Ortiz

    So Filip,

    I have an interesting and quite possibly easy question for my scenario, to which I am applying your rather simple solution. So in your solution you deal with string values but I modified your solution to deal with returning proper json data in the response. Because I am dealing with specific c# object types where you cast “val” to a string I am actually casting my “val” to IEnumerable in OnActionExecuting. Then where you set the response content with the StringContent method I am setting it with new ObjectContent<IEnumerable>(val, new JsonMediaTypeFormatter()).

    This all works fine and well but assuming I want to decorate an action method which deals with a different object type this is not as modular. So my question is 2 part.
    1. Considering my solution works, would you consider it good practice? Take into account I think this is the only way to skin this cat…
    2. Most important, to make my code more modular, would you recommend that I decorate my action method with an additional parameter – Ie: [WebApiOutputCache(60, 400, false, typeOfObject)]?

    Thanks,
    Jon

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

      IF you look at the update at the top of the article, the post has evolved into a proper library https://github.com/filipw/AspNetWebApi-OutputCache/

      In there, the problem you mention is actually solved by caching the byte array + content type headers, instead of string. Then, when the cache gets hit, response is populated with that byte array and sent immediately to the client. This way is the most efficient as there is no unnecessary conversion.

      • Jonathan Ortiz

        Thank you kind sir!

      • Jonathan Ortiz

        Just finished hooking up your new and improved solution. Pretty awesome library! And it is much more performant too!

  • http://www.barryfogarty.com/ Barry Fogarty

    Can this library be used with a dynamic value for the timespans? I would like to control the cache duration via an app setting (e.g. web.config). I cannot figure out how to construct the class with a dynamic attribute value.

    I refer to the Nuget package version (V2) – however if you think its simplest to make a custom build of the library, please let me know what approach you would take.

  • Pingback: AppFabric OutputCaching - Bilim-Teknoloji | Pozitive Positive Pozitive.NeT