In this article, we are going to discuss how response caching works in ASP.NET Core.
Let’s get started.
What is Response Caching?
Response Caching is the mechanism of caching a server’s response by a browser or other clients. This will help in serving future requests for the same resources very quickly. Additionally, this will free up the server from processing and generating the same response multiple times.
ASP.NET Core uses the ResponseCache
attribute to set the response caching headers. Furthermore, we can use the Response Caching Middleware
to control the caching behavior from the server-side. Once we set the response caching headers, clients and other proxies can read those to determine how to cache the response from the server. As per the HTTP 1.1 Response Cache Specification, browsers, clients and proxies should conform to the caching headers.
HTTP Based Response Caching
Now, let’s talk about different HTTP Cache Directives
and how we can control the caching behavior using those directives.
The cache-control
is the primary header field that we use to specify the way response can be cached. When the cache-control header is present in the response, browsers, clients, and proxy servers should honor the headers and comply with them.
So, let’s inspect the common cache-control
directives:
- public – indicates that a cache can store the response either at the client-side or at a shared location
- private – indicates that only a private cache on the client-side may store the response, but not a shared cache
- no-cache – specifies that a cache must not use a stored response for any requests
- no-store – indicates that a cache must not store the response
no-cache and no-store may sound similar and even behave similarly, but there are some differences in the way browsers or clients understand both. We are going to discuss this in detail while working on the examples.
Apart from the cache-control, there are a few other headers that can control the caching behavior:
The pragma header is for backward compatibility with HTTP 1.0 specification and the no-cache directive. If we specify the cache-control header, it will ignore the pragma header.
Vary – indicates that it can send a cached response only if all the fields that we provide in the header match exactly with the previous request. If any of the fields changes, the server generates a new response.
HTTP Cache Directive Examples
Now, we are going to create an ASP.NET Core application and see the cache directives in action. Let’s create an ASP.NET Core Web API project and add a controller action method:
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { [HttpGet] public IActionResult Get() { return Ok($"Response was generated at {DateTime.Now}"); } }
Every time we execute this endpoint, we can observe that it appends the actual date and time of execution to the response. Now we are going to add response caching to the endpoint.
Important note: While testing response caching we should always click links on the web page or use swagger to execute the API endpoints in the browser. Otherwise, if we try to refresh the page or invoke the URI again, the browser will always request a new response from the server irrespective of the response cache settings.
ResponseCache Attribute
For an ASP.NET Core application, the ResponseCache
attribute defines the properties for setting the appropriate response caching headers. We can apply this attribute either at the controller level or for individual endpoints.
Let’s add the ResponseCache
attribute to the API endpoint:
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any)] public IActionResult Get() { return Ok($"Response was generated at {DateTime.Now}"); }
This Duration
property will produce the max-age
header, which we use to set the cache duration for 2 minutes (120 seconds). Similarly, the Location
property will set the location in the cache-control
header. Since we set the location as Any
, both the client and server will be able to cache the response, which is equivalent to the public
directive of the cache-control
header.
So, let’s invoke the API endpoint and verify these in the response headers:
cache-control: public,max-age=120
Additionally, when we invoke the endpoints multiple times, whenever the browser uses a cached response, it will indicate in the status code that the response is taken from the disk cache:
Status Code: 200 (from disk cache)
Now let’s explore the different options for the ResponseCache
parameters.
For changing the cache location to private
, we just need to change the Location
property’s value to ResponseCacheLocation.Client
:
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.Client)]
This will change the cache-control
header value to private
which means that only the client can cache the response:
cache-control: private,max-age=120
Now let’s update the Location
parameter to ResponseCacheLocation.None
:
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.None)]
This will set both the cache-control
and pragma
header to no-cache
, which means the client cannot use a cached response without revalidating with the server:
cache-control: no-cache,max-age=120 pragma: no-cache
In this configuration, we can verify that the browser does not use the cached response and the server generates a new response every time.
NoStore Property
Now let’s set the NoStore
property of the ResponseCache
attribute to true
:
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any, NoStore = true)]
This will set the cache-control
response header to no-store
which indicates that the client should not cache the response:
cache-control: no-store
Notice that this will override the value that we set for Location
. In this case, as well, the client will not cache the response.
Even though the no-cache
and no-store
values for cache-control
may give the same results while testing, the browsers, clients, and proxies interpret these headers differently. While no-store
indicates that the clients or proxies should not store the response or any part of it anywhere, no-cache
just means that the client should not use a cached response without revalidating with the server.
Now let’s see how the vary
header works.
VaryByHeader Property
For setting the vary
header, we can use the VaryByHeader
property of the ResponseCache
attribute:
[ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any, VaryByHeader = "User-Agent")]
Here we set the value for the VaryByHeader
property to User-Agent
, which will use the cached response as long as the request comes from the same client device. Once the client device changes, the User-Agent
value will be different and it will fetch a new response from the server. Let’s verify this.
First, let’s check the presence of the vary
header in the response headers:
vary: User-Agent
Additionally, on inspecting the request headers, let’s verify the user-agent
header, which corresponds to the device and browser that we use:
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
For testing purposes, let’s simulate a new request coming from a different device. For that, we can use the toggle device feature available with the developer tools of browsers such as Chrome and Edge:
After clicking on the toggle device button in the developer tools, we can choose the device that we want to simulate in the browser:
This will help us switch the browser to a phone or tablet simulation mode.
Once we switch the device, we can see that the server abandons the cached response and sends a new one. Also, note that the user-agent
header will be different:
user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
So by setting the VaryByHeader
property as User-Agent
, we can make the server send a new response for a new device.
VaryByQueryKeys Property
By using the VaryByQueryKeys
property of the ResponseCache
attribute, we can make the server send a new response when the specified query string parameters change. Of course, by specifying the value as “*”, we can generate a new response when any of the query string parameters changes.
For example, we might want to generate a new response when the Id
value changes in the URI:
.../values?id=1 .../values?id=2
For this, let’s modify the Get
action to include the id
parameter and provide the VaryByQueryKeys
property for the ResponseCache
attribute:
[HttpGet] [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new string[] { "id" })] public IActionResult Get(int id) { return Ok($"Response was generated for Id:{id} at {DateTime.Now}"); }
Remember that we need to enable the Response Caching Middleware
for setting the VaryByQueryKeys
property. Otherwise, the code will throw a runtime exception.
Response Caching Middleware
The Response Caching Middleware in ASP.NET Core app determines when a response can be cached, and stores and serves the response from the cache.
For enabling the Response Caching Middleware, we just need to add a couple of lines of code in the Program
class:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddResponseCaching(); ... var app = builder.Build(); ... app.MapControllers(); app.UseResponseCaching(); app.Run();
First, we need to add the middleware using the AddResponseCaching()
method and then we can configure the app to use the middleware with the UseResponseCaching()
method.
That’s it. We have enabled the Response Caching Middleware and the VaryByQueryKeys
property should work now.
Let’s run the application and navigate to the /values?id=1
endpoint:
Response was generated for Id:1 at 23-05-2022 12:07:22
We can see that we’ll receive a cached response as long as the query string is the same, but once we change the query string, the server will send a new response.
Let’s change the query string to /values?id=2
:
Response was generated for Id:2 at 23-05-2022 12:07:32
Note that there is no corresponding HTTP header for the VaryByQueryKeys
property. This property is an HTTP feature that is handled by Response Caching Middleware.
With that, we have covered all the common HTTP cache directives.
Cache Profiles
Instead of duplicating the response cache settings on individual controller action methods, we can configure cache profiles and reuse them across the application. Once we set up a cache profile, the application can use its values as defaults for the ResponseCache
attribute. Of course, we can override the defaults by specifying the properties on the attribute.
We can define a cache profile in the Program
class:
using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.CacheProfiles.Add("Cache2Mins", new CacheProfile() { Duration = 120, Location = ResponseCacheLocation.Any }); }); ...
Here we define a new cache profile called Cache2Mins
with a duration of 2 minutes and location as public
.
Now we can apply this cache profile to any controller or endpoint:
[HttpGet] [ResponseCache(CacheProfileName = "Cache2Mins")] public IActionResult Get() { return Ok($"Response was generated at {DateTime.Now}"); }
This will apply the defined cache-control
settings for the response:
cache-control: public,max-age=120
Instead of hard-coding the cache settings in the Program
class, we can define multiple cache profiles in the appsettings
file and make the response cache settings configurable:
{ "Logging": { ... }, "CacheProfiles": { "Cache2Mins": { "Duration": 120, "Location": "Any" } }, "AllowedHosts": "*" }
Then, we can read the cache profiles in the Program
class using the ConfigurationManager
class:
using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { var cacheProfiles = builder.Configuration .GetSection("CacheProfiles") .GetChildren(); foreach (var cacheProfile in cacheProfiles) { options.CacheProfiles .Add(cacheProfile.Key, cacheProfile.Get<CacheProfile>()); } });
This is a much better way of defining cache profiles, especially when we need to define multiple cache profiles for our application.
Conclusion
In this article, we discussed what response caching is and how it works in ASP.NET Core applications. Additionally, we have learned how to control the response caching behavior by setting the cache directives and using the response caching middleware. Finally, we learned how to define a cache profile and reuse it across the application.