TOC Caching
TOC Caching
Caching
can improve the quality and performance of our app a lot, but again, it is
something first we need to look at as soon as some bug appears. To cover
resource caching, we are going to work with HTTP Cache. Additionally, we
are going to talk about cache expiration, validation, and cache-control
headers.
The cache is a separate component that accepts requests from the API’s
consumer. It also accepts the response from the API and stores that
response if they are cacheable. Once the response is stored, if a
consumer requests the same response again, the response from the
cache should be served.
But the cache behaves differently depending on what cache type is used.
260
The client cache lives on the client (browser); thus, it is a private cache.
It is private because it is related to a single client. So every client
consuming our API has a private cache.
The gateway cache lives on the server and is a shared cache. This cache
is shared because the resources it caches are shared over different
clients.
The proxy cache is also a shared cache, but it doesn’t live on the server
nor the client side. It lives on the network.
With the private cache, if five clients request the same response for the
first time, every response will be served from the API and not from the
cache. But if they request the same response again, that response should
come from the cache (if it’s not expired). This is not the case with the
shared cache. The response from the first client is going to be cached,
and then the other four clients will receive the cached response if they
request it.
Before we start, let’s open Postman and modify the settings to support
caching:
261
In the General tab under Headers, we are going to turn off the Send no-
cache header:
262
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
You can see that the Cache-Control header was created with a public
cache and a duration of 60 seconds. But as we said, this is just a header;
we need a cache-store to cache the response. So, let’s add one.
builder.Services.ConfigureResponseCaching();
app.UseCors("CorsPolicy");
app.UseResponseCaching();
Now, we can start our application and send the same GetCompany
request. It will generate the Cache-Control header. After that, before 60
263
seconds pass, we are going to send the same request and inspect the
headers:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
You can see the additional Age header that indicates the number of
seconds the object has been stored in the cache. Basically, it means that
we received our second response from the cache-store.
Another way to confirm that is to wait 60 seconds to pass. After that, you
can send the request and inspect the console. You will see the SQL query
generated. But if you send a second request, you will find no new logs for
the SQL query. That’s because we are receiving our response from the
cache.
264
could lead to less readable code. Therefore, we can use CacheProfiles
to extract that configuration.
builder.Services.AddControllers(config =>
{
config.RespectBrowserAcceptHeader = true;
config.ReturnHttpNotAcceptable = true;
config.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
config.CacheProfiles.Add("120SecondsDuration", new CacheProfile { Duration =
120 });
})...
We only set up Duration, but you can add additional properties as well.
Now, let’s implement this profile on top of the Companies controller:
[Route("api/companies")]
[ApiController]
[ResponseCache(CacheProfileName = "120SecondsDuration")]
We have to mention that this cache rule will apply to all the actions inside
the controller except the ones that already have the ResponseCache
attribute applied.
That said, once we send the request to GetCompany , we will still have the
maximum age of 60. But once we send the request to GetCompanies :
https://localhost:5001/api/companies
There you go. Now, let’s talk about new Output caching introduced in
.NET 7.
265
Output caching is a mechanism for storing the output of a client's request
and serving the stored result on future requests. This significantly
improves an application's responsiveness by providing a faster response
to client requests and saving on repeated processing of the same output.
Caching is not a new concept in ASP.NET Core as we already have
response caching features. But this newly designed Output Caching API
(introduced in .NET 7) opens up a new horizon of possibilities with
caching.
266
Caching authenticated content
Tagging of cache contents and invalidating as a group
Purging cache
Delayed caching
Partial caching or donut caching
Before we start with the examples, let's comment out all the response
caching attributes in the CompaniesController and the line in the
Program class where we configure CacheProfiles for the response
caching.
To register the output caching with stores in our app, we have to modify
the ConfigureResponseCaching method in the ServiceExtensions
class:
Here, we modify the name of the method and then use the
AddOutputCache method to register the output caching mechanism.
Next, we have to modify the call to this configure method in the Program
class:
builder.Services.ConfigureOutputCaching();
267
And also call the UseOutputCache bellow the UseCors method to add the
middleware for caching:
//app.UseResponseCaching();
app.UseOutputCache();
That said, let's do the same thing as we did with the ResponseCache
attribute using the GetCompany action:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
But, if we send the same request before 60 seconds pass, we will see the
Age header for sure:
268
25.5.1 Using Policies With Output Caching
In a response caching section, we've used the profiles to set a profile and
use it with the ResponseCache attribute. We can do the same with output
caching, just this time, we have to use policies.
By using the AddBasePolicy method, we apply this base policy to all the
endpoints in our controllers. We can confirm that.
Currently, we are not using any caching attribute with the GetCompanies
action. But, if we send a request to that endpoint, and then another
before 10 seconds pass, we will see the Age header:
https://localhost:5001/api/companies
269
Of course, after 10 seconds, our app will fetch a new result from the
database.
The value of the Age header is higher than 10, which means that using
attributes override the base policy we just configured.
Besides the base policies, we can configure the named policies, which we
have to apply to specific endpoints.
[Route("api/companies")]
[ApiController]
//[ResponseCache(CacheProfileName = "120SecondsDuration")]
[OutputCache(PolicyName = "120SecondsDuration")]
public class CompaniesController : ControllerBase
270
Even though we applied this policy to the entire controller, the
GetCompany action will be cached for 60 seconds while other GET actions
will be cached for 120 seconds.
If, for example, we have created a base caching policy but don't want to
use caching mechanism on certain actions, we can use the NoStore
property of the OutputCache attribute:
[HttpGet("output-nocache")]
[OutputCache(NoStore = true)]
public IActionResult NonCachedOutput()
{
return Ok($"Output was generated at {DateTime.Now}");
}
For example, we can enable cache mechanism for the query string
parameter by using the VaryByQueryKeys property:
[HttpGet("output-varybykey")]
[OutputCache(VaryByQueryKeys = new[] { nameof(firstKey) })]
public IActionResult VaryByKey(string firstKey, string secondKey)
{
return Ok($"{firstKey} {secondKey} - retrieved at {DateTime.Now}");
}
271
[HttpGet("output-varybykey")]
[OutputCache(VaryByQueryKeys = new[] { nameof(firstKey) }, Duration = 10)]
public IActionResult VaryByKey(string firstKey, string secondKey)
But, a much better way would be to create a policy with multiple rules:
services.AddOutputCache(opt =>
{
opt.AddPolicy("QueryParamDuration", p =>
p.Expire(TimeSpan.FromSeconds(10))
.SetVaryByQuery("firstKey"));
});
[HttpGet("output-varybykey")]
[OutputCache(PolicyName = "QueryParamDuration")]
public IActionResult VaryByKey(string firstKey, string secondKey)
We should also be aware that next to this “vary” key, we have some
additional ones that we can use similarly to the previous one:
return Ok(company);
}
272
As soon as request arrives, the server creates an etag value and adds it
to the ETag header.
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
If we send another one before 60 seconds expire, we will find the same
ETag value and the Age header as well. Also, pay attention that the status
code of the response is 200 OK.
Now, as soon as we send another request to the same endpoint, but this
time with the If-None-Match header with the same value as Etag’s, we
will see a different status code:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
273
We get this status code because the resource we are fetching is the same.
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3
And then, while caching session is still active (no more than 60 seconds
passed) we send another GET request with the same If-None-Match
value:
274
We can see that we get 200 OK response and not 304 because our
company is modified now.
There are a lot more functionalities that output caching provides for us,
but those we covered here should give you a good understanding of how
output caching works and how to use it.
275