Http Response Caching Attribute in .NET Core Web APIs
.NET Core Web API and Response Cache Attribute

Http Response Caching Attribute in .NET Core Web APIs

Now a days, systems are expected to be prompt. You click on something and immediately you want to see the result. But for web applications, designing performant applications might be tricky sometimes.

In this article, let’s understand some details about response caching in .NET Core Web APIs.

Why Caching ?

There are various factors involved when it comes to performance. Some of those factors are:

  • Network Bandwidth (how much data can be transmitted by the web server per second),
  • Network Latency (time taken by request for travelling),
  • Concurrency (how many requests server can process concurrently),
  • How many resources are available on server, etc.

One of the common solution that is applied to make application performant is response caching. The response, generated by server after processing the request, is cached at different levels so that it can be reused if same request is made. Depending on where the response is cached, it might save some bandwidth for web server and it might reduce network latency.

Apart from performance, if the server decide to cache the generated response, it might also reduce server’s throughput. The server might serve more number of requests per second if some processing time is saved by caching the response somewhere.

Different Levels for Caching

There are different levels where responses can be cached. One of the common caching method is to cache the responses provided by some complex methods. This happens on the web server which processes the request.

Response caching can also happen on HTTP level. Any internet cache which adheres to HTTP 1.1 Caching specification, would respect the Cache-Control header to decide which requests and responses should be cached.

HTTP Level caching can happen anywhere – from internet proxies, browsers, etc.

Cache-Control Header

Let’s quickly understand cache-control header.

Cache-Control is used to specify caching directives. These directives specify and control how the requests and responses are cached, when they are travelling through internet proxies.

Below are some common Cache-Control directives

  • public, cache may store the response.
  • private, public internet caches must not store this response. But Private internet caches may store and reuse the response.
  • max-age, client would not accept the response if age is greater than specified number of seconds
  • no-cache, on requests, it means caches should not return stored response. This means requests would be processed by original web server to regenerate the response. On response it means this response should not be stored in cache.
  • no-store, internet cache must not store request or response.

There are some other caching headers too which affects caching:

  • Expires, time after which the cached response would be considered as stale
  • Vary, which is collection of field. This header specifies that cached response should be returned ONLY IF all the vary fields from cached response match the vary header fields from the new request.
  • Pragma, this is for specifying no-cache behavior. This only exists for backward compatibility with HTTP 1.0.
  • Age specifies time in seconds. This time represents the time for which the response has been in the cache.

Response Cache Attribute

ResponseCacheAttribute specifies the parameters necessary for setting appropriate headers in response caching. This attribute can be set on the individual actions.

Before seeing code example, let’s look at some basic properties from this attribute.

NoStore

This overrides most of the other properties. It can be set to either true or false.

If NoStore is trueCache-Control, is set to no-store.

If NoStore is false and Location is NoneCache-Control, and Pragma are set to no-cache.

Location and Duration

To enable caching, Duration must be set to a positive value.

Another property Location must be set to any one of the below values:

  • Any (the default) – this means Cache-Control would be set to public, meaning the responses can be cached at client or any intermediate internet proxy.
  • Client – means Cache-Control would be set to private meaning the response should be cached only at the client.

Apart from two values, Location can also be set to None, which would result in not caching the response at all (as stated in previous section).

Vary

VaryByHeader or VaryByQueryKeys can be set on ResponseCacheAttribute to add Vary header in the response.

VaryByHeader takes another HTTP header name as input. It can be useful to instruct caching servers to return response only if specified header values from cached response and from new request are matching.

e.g. if VaryByHeader input is user-agent, it instructs caching server to return cached response only if user-agent head of new requests matches with user-agent header from the cached response.

VaryByQueryKeys instructs to return the cached response only if query string values are different. Either a specific query string key can be specified in the attribute, or * can be used if all query strings should be matched.

Code Examples

Below code example shows how the attribute can be used on API controller actions and comments show which headers are generated.

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
// cache-control: no-store,no-cache
// pragma: no-cache
[HttpGet("nostore")]
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
public IEnumerable<WeatherForecast> Get()
{
return GetResponse();
}
// cache-control: no-cache,max-age=0
// pragma: no-cache
[HttpGet("store")]
[ResponseCache(NoStore = false, Duration = 0, Location = ResponseCacheLocation.None)]
public IEnumerable<WeatherForecast> GetTwo()
{
return GetResponse();
}
// cache-control: public,max-age=60
[HttpGet("Any")]
[ResponseCache(NoStore = false, Location = ResponseCacheLocation.Any, Duration = 60)]
public IEnumerable<WeatherForecast> GetThree()
{
return GetResponse();
}
// cache-control: private,max-age=60
[HttpGet("Client")]
[ResponseCache(NoStore = false, Location = ResponseCacheLocation.Client, Duration = 60)]
public IEnumerable<WeatherForecast> GetFour()
{
return GetResponse();
}
// cache-control: public,max-age=30
// vary: User-Agent
[HttpGet("VaryByHeader")]
[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]
public IEnumerable<WeatherForecast> GetFive()
{
return GetResponse();
}
// NOTE: This requres ResposneCachingMiddleware
// cache-control: public,max-age=30
[HttpGet("VaryByQueryKeys")]
[ResponseCache(VaryByQueryKeys = new string[] { "*" }, Duration = 30)]
public IEnumerable<WeatherForecast> GetSix()
{
return GetResponse();
}
private static IEnumerable<WeatherForecast> GetResponse()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}

I hope you find this information useful. Let me know your thoughts.

Leave a Reply