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,
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 theAvatarController
to see how to validate an in-memory cache entry usingEtag
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.