ASP.NET Core Response Caching

Posted by ryansouthgate on 29 Nov 2022

In this post I’m going to take a quick look at the built in Response Caching feature in ASP.NET Core.

Sometimes, as developers, we’re drawn to complexity. I was recently keeping tabs on a website I build/manage. It runs in AWS, the JSON REST API is served by Lambdas. The site has a bunch of (mostly) static data, stuff that doesn’t change frequently. Think Countires, Currencies, (internal) statuses etc. Currently the user logs in to the front-end SPA (single page application), and all this information is requested, for every user, every time.

Now, this is ok for relatively small traffic sites, however sometimes this site can easily rack up over 600 new users a week, not to mention, you’re charged for each invocation on an AWS Lambda (which also opens Db connections too).

We all want quick wins, things that can be implemented fast, come built into the framework we’re using and battle-tested.

First Thought

My initial thought, around making this more efficient, was around the server side. All this static information is queried from the database, serialised to JSON and then served to the User. We’re using AWS API Gateway too, which means that there might be opportunity to use something pre-built in there. Maybe they have a built in caching mechanism?

Whilst this would reduce load on our Lambdas (API endpoints), this isn’t the best we can do. I took a quick look around API Gateway caching documentation and it started to talk about “pricing” and setting up other things. Sounds like far too much hassle, I’m after a quick win here!

Second

My second thought was implementing some browser-side caching in the Angular SPA. I could dive into the “NgRx Effects” which were handling the HTTP request/responses. Maybe wrap those in some fancy code which caches certain responses (using localstorage). Whilst this would be easier than the AWS API Gateway caching above, still, it’s writing a lot of code. There has to be a better way…

Third

It dawned on me, I’m not the first person ever to have this problem, this has already been solved by better developers than me.

And it’s something that’s been built-in to browsers for years (it’s in the spec HTTP/1.1 - which was released in 1997)

Enter Cache-Control. When requesting data (html, js, css, json etc) you send back a time (in seconds) which tells the browser, how long to cache the response for. If for example you set it to 5 days, and a user makes that same request a few times in the next hour. Then their browser wont bother trying to contact your server, instead, it’ll find the response in it’s own cache (usually on your hard drive) and retrieve that instead.

If you’re using ASP.NET Core, this is going to be nice and easy to set up. And there’s no changes that need to be made in your front end code (if you have any)

Microsoft has great documentation about Response Caching, but I’m going to distill this further, using the simplest example.

Working on ASP.NET Core

There’s a couple of very small things to set up in your project, I’ve added comments inline.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // The usual stuff
    services.AddControllers();

    // Add the response caching middleware
    services.AddResponseCaching();

    ...more services you register etc
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.UseHttpsRedirection();
    app.UseRouting();

    // If used, this need to be called before UseResponseCaching
    app.UseCors();
    // Use the response caching
    app.UseResponseCaching();

    .. again, other stuff you might be using
    app.UseAuthentication();
    app.UseAuthorization();
}

And now to configure caching for our individual endpoints. This attribute can be used across JSON/XML and even MVC (view) endpoints. CountriesController.cs

[ApiController]
public class CountriesController : ControllerBase
{
    [Route("countries")]
    [HttpGet]
    // Duration in Seconds (86400 = 1 day)
    // ResponseCacheLocation.Any = Browsers and Proxies can cache
    // ResponseCacheLocation.Client = Only browsers should cache
    // ResponseCacheLocation.None = Don't cache anywhere
    [ResponseCache(Duration = 86400, Location = ResponseCacheLocation.Any)]
    public IActionResult GetCountries()
    {
        // Go to Db and get the Countries
        return Ok(_dbContext.Countries.AsNoTracking());
    }
}

The benefits

For a couple of lines of code in the server, the benefits here are huge. We’ve removed a ton of latency for our end user. Instead of them getting Countries, Currencies etc each time they log in, they are downloaded once and then cached (in my case for 100 days - how many times a year do new Countries pop up?).

There are no changes that need to be made client side. Your browser just starts caching things, for free! Your JS that makes API requests, wont need to be changed. If your browser has spotted a Cache-Control header, and your JS code asks for that resource again, it gets given the cached version, super fast!

Looking at the Network Tab in my browser developer tools (F12), before I made the change, I was seeing the request times for GetCountries at about 400-500ms (thanks Lambda and cold-starts). After the change, when the browser serves from the cache, it does so in ~5ms.

This is a cheap change and saves you bandwidth, server processing and money. It saves your users time, your site will feel quicker and more responsive too.

Side note: this blog also uses the Cache-Control response header. Press F12 (and refresh the page) and see it on some of the assets that download. Cache-Control is not specific to ASP.NET Core, the server side framework you are using, very likely has this too.

Sometimes, taking a step back, using tools that are built into browsers, and profitting, feels really good!



comments powered by Disqus