Ultimate ASP.NET Core Web API 25 CACHING

25 CACHING
25 缓存

In this section, we are going to learn about caching resources. 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.‌
在本节中,我们将学习缓存资源。缓存可以大大提高我们应用程序的质量和性能,但同样,一旦出现一些错误,我们首先需要查看它。为了涵盖资源缓存,我们将使用 HTTP Cache。此外,我们将讨论缓存过期、验证和缓存控制标头。

25.1 About Caching

25.1 关于缓存

We want to use cache in our app because it can significantly improve performance. Otherwise, it would be useless. The main goal of caching is to eliminate the need to send requests towards the API in many cases and also to send full responses in other cases.‌
我们希望在我们的应用程序中使用 cache,因为它可以显著提高性能。否则,它将毫无用处。缓存的主要目标是在许多情况下无需向 API 发送请求,在其他情况下也无需发送完整响应。

To reduce the number of sent requests, caching uses the expiration mechanism, which helps reduce network round trips. Furthermore, to eliminate the need to send full responses, the cache uses the validation mechanism, which reduces network bandwidth. We can now see why these two are so important when caching resources.
为了减少发送的请求数,缓存使用过期机制,这有助于减少网络往返次数。此外,为了消除发送完整响应的需要,缓存使用验证机制,这减少了网络带宽。我们现在可以看到为什么这两个在缓存资源时如此重要。

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.
缓存是一个单独的组件,它接受来自 API 使用者的请求。它还接受来自 API 的响应,并存储该响应(如果它们是可缓存的)。存储响应后,如果使用者再次请求相同的响应,则应提供来自缓存的响应。

But the cache behaves differently depending on what cache type is used.
但是,缓存的行为会有所不同,具体取决于所使用的缓存类型。

25.1.1 Cache Types‌

25.1.1 缓存类型

There are three types of caches: Client Cache, Gateway Cache, and Proxy Cache.
有三种类型的缓存:客户端缓存、网关缓存和代理缓存。

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.
客户端缓存位于客户端(浏览器)上;因此,它是一个私有缓存。它是私有的,因为它与单个客户端相关。因此,每个使用我们 API 的客户端都有一个私有缓存。

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.
使用私有缓存时,如果五个客户端首次请求相同的响应,则每个响应都将从 API 而不是缓存中提供。但是,如果他们再次请求相同的响应,则该响应应来自缓存(如果它未过期)。共享缓存不是这种情况。来自第一个客户端的响应将被缓存,然后其他四个客户端将收到缓存的响应(如果它们请求)。

25.1.2 Response Cache Attribute‌

25.1.2 响应缓存属性

So, to cache some resources, we have to know whether or not it’s cacheable. The response header helps us with that. The one that is used most often is Cache-Control: Cache-Control: max-age=180. This states that the response should be cached for 180 seconds. For that, we use the ResponseCache attribute. But of course, this is just a header. If we want to cache something, we need a cache-store. For our example, we are going to use Response caching middleware provided by ASP.NET Core.
所以,要缓存一些资源,我们必须知道它是否是可缓存的。响应标头可以帮助我们解决这个问题。最常用的是 Cache-Control: Cache-Control: max-age=180。这表示响应应缓存 180 秒。为此,我们使用 ResponseCache 属性。但当然,这只是一个标题。如果我们想缓存一些东西,我们需要一个 cache-store。对于我们的示例,我们将使用 ASP.NET Core 提供的响应缓存中间件。

25.2 Adding Cache Headers

25.2 添加缓存 Headers

Before we start, let’s open Postman and modify the settings to support caching:‌
在开始之前,让我们打开 Postman 并修改设置以支持缓存:

alt text

In the General tab under Headers, we are going to turn off the Send no- cache header:
在 Headers 下的 General 选项卡中,我们将关闭 Send no- cache 标头:

alt text

Great. We can move on.
我们可以继续前进。

Let’s assume we want to use the ResponseCache attribute to cache the result from the GetCompany action:
假设我们要使用 ResponseCache 属性来缓存 GetCompany作的结果:

alt text

It is obvious that we can work with different properties in the ResponseCache attribute — but for now, we are going to use Duration only:
很明显,我们可以在 ResponseCache 属性中使用不同的属性 — 但现在,我们只使用 Duration:

[HttpGet("{id}", Name = "CompanyById")] 
[ResponseCache(Duration = 60)] 
public async Task<IActionResult> GetCompany(Guid id)

And that is it. We can inspect our result now:
就是这样。我们现在可以检查我们的结果:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

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.
您可以看到,Cache-Control 标头是使用公共缓存创建的,持续时间为 60 秒。但正如我们所说,这只是一个标题;我们需要一个 cache-store 来缓存响应。那么,让我们添加一个。

25.3 Adding Cache-Store

25.3 添加 Cache-Store

The first thing we are going to do is add an extension method in the‌ ServiceExtensions class:
我们要做的第一件事是在 ServiceExtensions 类中添加一个扩展方法:

public static void ConfigureResponseCaching(this IServiceCollection services) => services.AddResponseCaching();

We register response caching in the IOC container, and now we have to call this method in the Program class:
我们在 IOC 容器中注册响应缓存,现在我们必须在 Program 类中调用此方法:

builder.Services.ConfigureResponseCaching();

Additionally, we have to add caching to the application middleware right below UseCors() because Microsoft recommends having UseCors before UseResponseCaching, and as we learned in the section 1.8, order is very important for the middleware execution:
此外,我们必须将缓存添加到应用程序中间件的 UseCors() 正下方,因为 Microsoft 建议在 UseResponseCached 之前使用 UseCors,正如我们在第 1.8 节中学到的那样,顺序对于中间件的执行非常重要:

app.UseResponseCaching();
app.UseCors("CorsPolicy");

Now, we can start our application and send the same GetCompany request. It will generate the Cache-Control header. After that, before 60 seconds pass, we are going to send the same request and inspect the headers:
现在,我们可以启动应用程序并发送相同的 GetCompany 请求。它将生成 Cache-Control 标头。之后,在 60 秒过去之前,我们将发送相同的请求并检查标头:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

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.
您可以看到额外的 Age 标头,该标头指示对象在缓存中存储的秒数。基本上,这意味着我们收到了来自 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.
另一种确认方法是等待 60 秒。之后,您可以发送请求并检查控制台。您将看到生成的 SQL 查询。但是,如果您发送第二个请求,则不会找到 SQL 查询的新日志。那是因为我们正在从缓存中接收响应。

Additionally, with every subsequent request within 60 seconds, the Age property will increment. After the expiration period passes, the response will be sent from the API, cached again, and the Age header will not be generated. You will also see new logs in the console.
此外,对于 60 秒内的每个后续请求,Age 属性将递增。过期期限过后,将从 API 发送响应,再次缓存,并且不会生成 Age 标头。您还将在控制台中看到新日志。

Furthermore, we can use cache profiles to apply the same rules to different resources. If you look at the picture that shows all the properties we can use with ResponseCacheAttribute, you can see that there are a lot of properties. Configuring all of them on top of the action or controller could lead to less readable code. Therefore, we can use CacheProfiles to extract that configuration.
此外,我们可以使用缓存配置文件将相同的规则应用于不同的资源。如果你看一下图片,它显示了我们可以与 ResponseCacheAttribute 一起使用的所有属性,你可以看到有很多属性。在 action 或 controller 之上配置所有这些可能会导致代码的可读性降低。因此,我们可以使用 CacheProfiles 来提取该配置。

To do that, we are going to modify the AddControllers method:
为此,我们将修改 AddControllers 方法:

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:
我们只设置了 Duration,但您也可以添加其他属性。现在,让我们在 Companies 控制器上实现这个配置文件:

[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.
我们必须提到,此缓存规则将应用于控制器内的所有作,但已应用 ResponseCache 属性的作除外。

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:
也就是说,一旦我们将请求发送到 GetCompany,我们的最大年龄仍然是 60 岁。但是,一旦我们将请求发送到 GetCompanies:
https://localhost:5001/api/companies

alt text

There you go. Now, let’s talk some more about the Expiration and Validation models.
现在,让我们更多地讨论 Expiration 和 Validation 模型。

25.4 Expiration Model

25.4 过期模型

The expiration model allows the server to recognize whether or not the response has expired. As long as the response is fresh, it will be served from the cache. To achieve that, the Cache-Control header is used. We have seen this in the previous example.‌
过期模型允许服务器识别响应是否已过期。只要响应是最新的,它就会从缓存中提供。为此,使用了 Cache-Control 标头。我们在前面的示例中已经看到了这一点。

Let’s look at the diagram to see how caching works:
让我们看一下图表,看看缓存是如何工作的:

alt text

So, the client sends a request to get companies. There is no cached version of that response; therefore, the request is forwarded to the API. The API returns the response with the Cache-Control header with a 10- minute expiration period; it is being stored in the cache and forwarded to the client.
因此,客户端发送请求以获取公司。该响应没有缓存版本;因此,请求将转发到 API。API 返回带有 Cache-Control 标头的响应,有效期为 10 分钟;它被存储在缓存中并转发到客户端。

If after two minutes, the same response has been requested:
如果两分钟后,请求了相同的响应:

alt text

We can see that the cached response was served with an additional Age header with a value of 120 seconds or two minutes. If this is a private cache, that is where it stops. That’s because the private cache is stored in the browser and another client will hit the API for the same response. But if this is a shared cache and another client requests the same response after an additional two minutes:
我们可以看到,缓存的响应使用值为 120 秒或 2 分钟的额外 Age 标头提供。如果这是私有缓存,则它是停止的地方。这是因为私有缓存存储在浏览器中,另一个客户端将点击 API 以获得相同的响应。但是,如果这是一个共享缓存,并且另一个客户端在额外的两分钟后请求相同的响应:

alt text

The response is served from the cache with an additional two minutes added to the Age header.
响应从缓存中提供,并向 Age 标头额外添加两分钟。

We saw how the Expiration model works, now let’s inspect the Validation model.
我们了解了 Expiration 模型的工作原理,现在让我们检查 Validation 模型。

25.5 Validation Model

25.5 验证模型

The validation model is used to validate the freshness of the response. So it checks if the response is cached and still usable. Let’s assume we have a shared cached GetCompany response for 30 minutes. If someone updates that company after five minutes, without validation the client would receive the wrong response for another 25 minutes — not the updated one.‌
验证模型用于验证响应的新鲜度。因此,它会检查响应是否已缓存且仍然可用。假设我们有一个共享缓存的 GetCompany 响应 30 分钟。如果有人在 5 分钟后更新了该公司,则客户将在 25 分钟内收到错误的响应,而不是更新后的响应。

To prevent that, we use validators. The HTTP standard advises using Last- Modified and ETag validators in combination if possible.
为了防止这种情况,我们使用验证器。HTTP 标准建议尽可能结合使用 Last- Modified 和 ETag 验证器。

Let’s see how validation works:
让我们看看验证是如何工作的:

alt text

So again, the client sends a request, it is not cached, and so it is forwarded to the API. Our API returns the response that contains the Etag and Last-Modified headers. That response is cached and forwarded to the client.
因此,客户端再次发送请求,该请求未被缓存,因此被转发到 API。我们的 API 返回包含 Etag 和 Last-Modified 标头的响应。该响应将被缓存并转发到客户端。

After two minutes, the client sends the same request:
两分钟后,客户端发送相同的请求:

alt text

So, the same request is sent, but we don’t know if the response is valid. Therefore, the cache forwards that request to the API with the additional headers If-None-Match — which is set to the Etag value — and If- Modified-Since — which is set to the Last-Modified value. If this request checks out against the validators, our API doesn’t have to recreate the same response; it just sends a 304 Not Modified status. After that, the regular response is served from the cache. Of course, if this doesn’t check out, a new response must be generated.
因此,发送了相同的请求,但我们不知道响应是否有效。因此,缓存将该请求转发到 API,其中包含额外的标头 If-None-Match(设置为 Etag 值)和 If- Modified-Since(设置为 Last-Modified 值)。如果此请求针对验证者进行检查,则我们的 API 不必重新创建相同的响应;它只是发送 304 Not Modified 状态。之后,将从缓存中提供常规响应。当然,如果这没有检查出来,则必须生成新的响应。

That brings us to the conclusion that for the shared cache if the response hasn’t been modified, that response has to be generated only once. Let’s see all of these in an example.
这使我们得出结论,对于共享缓存,如果响应尚未修改,则该响应只需生成一次。让我们通过一个示例来了解所有这些。

25.6 Supporting Validation

25.6 支持验证

To support validation, we are going to use the Marvin.Cache.Headers library. This library supports HTTP cache headers like Cache-Control, Expires, Etag, and Last-Modified and also implements validation and expiration models.‌
为了支持验证,我们将使用 Marvin.Cache.Headers 库。此库支持 Cache-Control、Expires、Etag 和 Last-Modified 等 HTTP 缓存标头,并且还实施验证和过期模型。

So, let’s install the Marvin.Cache.Headers library in the Presentation project, which will enable the reference for the main project as well. We are going to need it in both projects.
因此,让我们在 Presentation 项目中安装 Marvin.Cache.Headers 库,这也将启用主项目的引用。我们将在这两个项目中都需要它。

Now, let’s modify the ServiceExtensions class:
现在,让我们修改 ServiceExtensions 类:

public static void ConfigureHttpCacheHeaders(this IServiceCollection services) => services.AddHttpCacheHeaders();

We are going to add additional configuration later.
我们稍后将添加其他配置。

Then, let’s modify the Program class:
然后,让我们修改 Program 类:

builder.Services.ConfigureResponseCaching(); 
builder.Services.ConfigureHttpCacheHeaders();

And finally, let’s add HttpCacheHeaders to the request pipeline:
最后,让我们将 HttpCacheHeaders 添加到请求管道中:

app.UseResponseCaching(); 
app.UseHttpCacheHeaders();

To test this, we have to remove or comment out ResponseCache attributes in the CompaniesController. The installed library will provide that for us. Now, let’s send the GetCompany request:
为了测试这一点,我们必须删除或注释掉 CompaniesController 中的 ResponseCache 属性。已安装的库将为我们提供。现在,让我们发送 GetCompany 请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

We can see that we have all the required headers generated. The default expiration is set to 60 seconds and if we send this request one more time, we are going to get an additional Age header.
我们可以看到我们已经生成了所有必需的标头。默认过期时间设置为 60 秒,如果我们再次发送此请求,我们将获得一个额外的 Age 标头。

25.6.1 Configuration‌

25.6.1 配置

We can globally configure our expiration and validation headers. To do that, let’s modify the ConfigureHttpCacheHeaders method:
我们可以全局配置我们的 expiration 和 validation 标头。为此,让我们修改 ConfigureHttpCacheHeaders 方法:

public static void ConfigureHttpCacheHeaders(this IServiceCollection services) => services.AddHttpCacheHeaders(
(expirationOpt) => { expirationOpt.MaxAge = 65; expirationOpt.CacheLocation = CacheLocation.Private; }, (validationOpt) => { validationOpt.MustRevalidate = true; });    

After that, we are going to send the same request for a single company:
之后,我们将为单个公司发送相同的请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see that the changes are implemented. Now, this is a private cache with an age of 65 seconds. Because it is a private cache, our API won’t cache it. You can check the console again and see the SQL logs for each request you send.
您可以看到更改已实施。现在,这是一个存在时间为 65 秒的私有缓存。因为它是私有缓存,所以我们的 API 不会缓存它。您可以再次检查控制台并查看您发送的每个请求的 SQL 日志。

Other than global configuration, we can apply it on the resource level (on action or controller). The overriding rules are the same. Configuration on the action level will override the configuration on the controller or global level. Also, the configuration on the controller level will override the global level configuration.
除了全局配置之外,我们可以在资源级别(在 action 或 controller 上)应用它。覆盖规则是相同的。作级别的配置将覆盖控制器或全局级别的配置。此外,控制器级别的配置将覆盖全局级别的配置。

To apply a resource level configuration, we have to use the HttpCacheExpiration and HttpCacheValidation attributes:
要应用资源级别配置,我们必须使用 HttpCacheExpiration 和 HttpCacheValidation 属性:

[HttpGet("{id}", Name = "CompanyById")] [HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 60)] [HttpCacheValidation(MustRevalidate = false)] public async Task<IActionResult> GetCompany(Guid id)

Once we send the GetCompanies request, we are going to see global values:
发送 GetCompanies 请求后,我们将看到全局值:

alt text

But if we send the GetCompany request:
但是,如果我们发送 GetCompany 请求:

alt text

You can see that it is public and you can send the same request again to see the Age header for the cached response.
您可以看到它是公有的,并且可以再次发送相同的请求以查看缓存响应的 Age 标头。

25.7 Using ETag and Validation

25.7 使用 ETag 和验证

First, we have to mention that the ResponseCaching library doesn’t correctly implement the validation model. Also, using the authorization header is a problem. We are going to show you alternatives later. But for now, we can simulate how validation with Etag should work.‌
首先,我们必须提到 ResponseCaching 库没有正确实现验证模型。此外,使用 authorization 标头也是一个问题。我们稍后将向您展示替代方案。但现在,我们可以模拟使用 Etag 进行验证应该如何工作。

So, let’s restart our app to have a fresh application, and send a GetCompany request one more time. In a header, we are going to get our ETag. Let’s copy the Etag’s value and use another GetCompany request:
因此,让我们重新启动应用程序以获得新的应用程序,并再次发送 GetCompany 请求。在 header 中,我们将获取我们的 ETag。让我们复制 Etag 的值并使用另一个 GetCompany 请求:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

We send the If-None-Match tag with the value of our Etag. And we can see as a result we get 304 Not Modified.
我们发送带有 Etag 值的 If-None-Match 标签。我们可以看到,结果是 304 Not Modified。

But this is not a valid situation. As we said, the client should send a valid request and it is up to the Cache to add an If-None-Match tag. In our example, which we sent from Postman, we simulated that. Then, it is up to the server to return a 304 message to the cache and then the cache should return the same response.
但这不是一个有效的情况。正如我们所说,客户端应该发送一个有效的请求,并且由 Cache 添加 If-None-Match 标签。在我们从 Postman 发送的示例中,我们对此进行了模拟。然后,由服务器将 304 消息返回到缓存,然后缓存应返回相同的响应。

But anyhow, we have managed to show you how validation works. If we update that company:
但无论如何,我们已经设法向您展示了验证的工作原理。如果我们更新该公司:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

And then send the same request with the same If-None-Match value:
然后发送具有相同 If-None-Match 值的相同请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

You can see that we get 200 OK and if we inspect Headers, we will find that ETag is different because the resource changed:
你可以看到我们得到 200 OK,如果我们检查 Headers,我们会发现 ETag 是不同的,因为资源发生了变化:

alt text

So, we saw how validation works and also concluded that the ResponseCaching library is not that good for validation — it is much better for just expiration.
因此,我们看到了验证的工作原理,并得出结论,ResponseCaching 库对于验证来说不是那么好 — 它更适合于过期。

But then, what are the alternatives? There are a lot of alternatives, such as:
但是,还有哪些选择呢?有很多选择,例如:

• Varnish - https://varnish-cache.org/

• Apache Traffic Server - https://trafficserver.apache.org/

• Squid - http://www.squid-cache.org/

They implement caching correctly. And if you want to have expiration and validation, you should combine them with the Marvin library and you are good to go. But those servers are not that trivial to implement.
它们正确地实现了缓存。如果你想拥有过期和验证,你应该将它们与 Marvin 库结合起来,一切顺利。但这些服务器并不是那么容易实现。

There is another option: CDN (Content Delivery Network). CDN uses HTTP caching and is used by various sites on the internet. The good thing with CDN is we don’t need to set up a cache server by ourselves, but unfortunately, we have to pay for it. The previous cache servers we presented are free to use. So, it’s up to you to decide what suits you best.
还有另一个选项:CDN(内容交付网络)。CDN 使用 HTTP 缓存,并被 Internet 上的各种站点使用。CDN 的好处是我们不需要自己设置缓存服务器,但不幸的是,我们必须为此付费。我们之前介绍的缓存服务器可以免费使用。因此,由您决定什么最适合您。

Leave a Reply

Your email address will not be published. Required fields are marked *