Cache Headers for Static Files & File Action Results



There are only two hard things in Computer Science: cache invalidation and naming things. - Phil Karlton

Static Files:

The StaticFiles middleware is responsible for serving static files (CSS, javascript, image, etc.) from a configurable file location (typically from the wwwroot folder). A simple HTTP call for a static file would look something like the following,

If you try the same thing from a browser, for successive requests you will get a 304 Not Modified. That is because of the ETag and Last-Modified headers. By default, the StaticFiles middleware uses these headers for cache invalidation purposes.

The integrity of the cache is  assessed by comparing the If-Modified-Since and If-None-Match header values with the previously sent Etag and Last-Modified header values respectively. If it matches; means our cache is still valid and the server sends a 304 Not Modified status to the browser. On the browser's end, it handles the status by serving up the static file from its cache.

The reason behind taking an approach like this is that you don't want your server getting worked up for handling a file that doesn't change frequently.

You might be wondering, to check the integrity of the cache, we are calling the server again. So how does it benefit me? Although it's a new HTTP request, the request payload is much less than a full-body request. It checks if the request passes cache validation. A valid cache will make a request to opt-out of further processing.

Along the way, if you want to have total control over the caching of a static file, you can configure the StaticFiles middleware with StaticFileOptions. The following will remove the ETag and Last-Modified headers and only add a Cache-Control header,

app.UseStaticFiles(new StaticFileOptions()
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Remove("ETag");
        ctx.Context.Response.Headers.Remove("Last-Modified");
        ctx.Context.Response.Headers.Append("Cache-Control", "private,max-age=100");
    }
});
Startup.cs

The cache invalidation part is now gone forever, and the file is in the memory-cache for 100 seconds only. After 100 seconds, the browser will again request the server to get the file like it was the first time.

File Action Results:

You typically use one of the following FileResult to write a file to the response,

Method Description
FileContentResult Sends the contents of a binary file to the response.
FileStreamResult Represents an ActionResult that when executed will write a file from a stream to the response.
VirtualFileResult A FileResult that on execution writes the file specified using a virtual path to the response using mechanisms provided by the host.

All of these FileResult inherit from the IActionResult interface. So you would want to use a less derived type for the return type,

public IActionResult Download()
{
    return File("/assets/guitar.mp3", 
        "audio/mpeg", 
        "guitar.mp3");
}

Using FileResult in earlier versions of .NET Core (1.*), you could have served files. However, there wasn't any way of configuring cache headers for the responses.

Now, FileResult can be overloaded with the following parameters,

  • DateTimeOffset? lastModified
  • EntityTagHeaderValue entityTag
  • bool enableRangeProcessing

With help of these parameters now you can add cache headers to any file that you want to write to the response. Here is an example of how you will do it,

[HttpGet("download")]
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Client)]
public IActionResult Download()
{
    return File("/assets/guitar.mp3", 
        "audio/mpeg", 
        "guitar.mp3",
        lastModified: DateTime.UtcNow.AddSeconds(-5),
        entityTag: new EntityTagHeaderValue($"\"{Convert.ToBase64String(Guid.NewGuid().ToByteArray())}\""),
        enableRangeProcessing: true);
}

This is just a simple example. In production, you would set entityTag and lastModified to some real-world values.

You can take a look at the AvatarController to see how to validate an in-memory cache entry using Etag

How do you add other cache headers like Cache-Control in this scenario? Well you can use the Response Caching Middleware and decorate your action with the ResponseCache attribute like the following,

[HttpGet("download")]
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Client)]
public IActionResult Download()
{
    return File("/assets/guitar.mp3",
        "audio/mpeg",
        "guitar.mp3",
        lastModified: DateTime.UtcNow.AddSeconds(-5),
         entityTag: new EntityTagHeaderValue($"\"{Convert.ToBase64String(Guid.NewGuid().ToByteArray())}\""),
        enableRangeProcessing: true);
}

For static files, the value for entityTag and lastModified is calculated using the following code snippet,

if (_fileInfo.Exists)
{
    _length = _fileInfo.Length;

    DateTimeOffset last = _fileInfo.LastModified;
            // Truncate to the second.
    _lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();

     long etagHash = _lastModified.ToFileTime() ^ _length;
     _etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}

Anyways, you can also request partial resources by adding a Range header in the request. The value of the header would be a bytes=to-from range. Following is an example of the Range header in action,

Note: every resource can be partially delivered. Some partial requests can make the resource unreadable/corrupted. So, use the Range header wisely.
fiyazbinhasan/RangeEtagsLastModified
Contribute to fiyazbinhasan/RangeEtagsLastModified development by creating an account on GitHub.
Response Caching Middleware in ASP.NET Core
Learn how to configure and use Response Caching Middleware in ASP.NET Core.
Fiyaz Bin Hasan (Fizz) on LinkedIn: Caching - Ins & Outs - Fiyaz Hasan
At #Cefalo, it’s not always about coding! Sometimes we do take time for R&D and stuff on topics we are interested in. Then we share our insights on what...

https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching