Caching database images in the browser on Asp.Net Core

To build responsive and user-friendly web applications, caching even small files is an easy way to reduce load times and improve the overall user experience.

Caching can get very complex, very quickly, but for serving content that does not change regularly (if at all), and does not change depending on the current user, it is as simple as adding an additional HTTP header to the response.

In Asp.Net Core, we can store static files in the wwwroot directory, and using the UseStaticFiles extension method you can append the Cache-Control header to every outgoing response, as demonstrated here.

That’s great for site logos, footer images, static homepage jumbotrons etc, but what about content that isn’t static, like user-generated content that’s stored in a database? Storing images in a database is not always advisable, and developers are split down the middle on database vs. file system, but its generally OK for smaller images.

Let’s say I have a table for Suppliers, and each Supplier can upload an icon to be displayed in search results and on their profile page. This icon is stored as a byte array in the database. In my controller I create an action that takes an int argument (the unique id of the image), queries the database and returns the file contents:

public class ContentController : Controller
{
public IRepository Repository { get; set; }

  // get: Content/Image/{id}
  public async Task Image(int id, string extension = "png")
  {
  // Query the database for the image
  byte[] image = await Repository.GetImageAsync(id);
 
  // Return a 404 response if no image exists
  if (image == null)
  return NotFound();

  return File(image, $"image/{extension}");
  }
}

This action returns an image, but every request for the same image it will create a new Http Request, and cause the image to be downloaded. Inspecting the Http Response tells us that no Cache-Control header is appended:

no-cache

If you’re asking “What on Earth is a Cache-Control header”, then have a look at the MDN Documentation on the subject. There are a whole host of different values for the Cache-Control header, and this documentation should help you make the most appropriate choice.

To fix this, simply access the Cache Control header and set it to a valid value:

public class ContentController
{
public IRepository Repository { get; set; }
public const int CacheAgeSeconds = 60 * 60 * 24 * 30 // 30 days

// get: Content/Image/{id}
public async Task Image(int id, string extension = "png")
{
// Query the database for the image
byte[] image = await Repository.GetImageAsync(id);

if (image == null)
return NotFound();

Response.Headers["Cache-Control"] = $"public,max-age={CacheAgeSeconds}";

return File(image, $"image/{extension}");
}
}

Now when we inspect the Http Response, we see that the Cache-Control header has been added.

cache-control

If the page is refreshed, the browser will realise that it has the image in it’s local cache, and that the date is still valid, so the response will look like this:

cache-control-refresh

For the odd image, this will result in an almost imperceptible performance improvement, but taking the example above, let’s say a user is constantly returning to the Suppliers listing page: since the Supplier icons are cached locally, the page will load much quicker than the first load or without caching enabled.

Of course the correct answer to “Should I implement caching in my website?” is always “It Depends”, so let’s look at some of the Pros and Cons of this solution.

Pros:

  • Faster page loads! This one should be obvious…
  • Fewer requests to your server
  • Fewer medium and large files being requested
  • Cached images load almost instantaneously, so your page won’t jump about as much as images are rendered seconds after the DOM’s loaded

Cons:

  • If the file changes, the page has no way of knowing (there are solutions to this, known as Cache Busting)
  • If you need to display different images for different users, this method will simply display the cached image, if available, with no regard for the current User. This might not be appropriate on multi-user machines.
  • No Authentication/Authorisation – some files may only be available to Authenticated or Authorised users.

What I have described is a very simple way to ask the browser to keep a local copy of a file. The world of caching gets much more complex the further up the request pipeline you go, but with that complexity comes much more flexibility and finer-grained control over your caching policy. One place to start for Asp.Net Core applications would be (as usual) the documentation, which outlines the steps to implement response-caching middleware, such as enabling you to vary the response based on request query keys. Another, perhaps simpler, way of getting started with response caching is to use the Cache Tag Helper in your Razor views.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this:
search previous next tag category expand menu location phone mail time cart zoom edit close