ASP.NET Core MVC 的 Cache control 与 Response cache middleware

在 web 环境下,可以发生 cache 的地方有三种类型,第一是用户端,也就是发出 HTTP request 的地方,简单的说就是你电脑上的 Chrome, Firefox 这类的浏览器.第二是 network proxy,它是网络上的一种服务,可将内容暂存下来,通常是由你的 ISP 公司所提供的服务.第三是服务器端,也就是 cache content 的来源产生地,也就是 HTTP response 的起源,简单的说就是你漤览器所连接的目的地.依目前的 HTTP 规格而言,ASP.NET Core 所能够控制 cache 的地方就是这三个.Response cache middleware 是用在服务器端用的组件,用来定义服务器端 cache 的规则和行为.这篇文章将从用户端的 cache 先谈起,然后下一篇文章再谈到服务器端的 Response cache middleware.

在 HTTP request / response 的过程中,cache 行为都定义在 HTTP 1.1 规格中的 Cache-Control.这是 HTTP Header 里的一员.你可以在 HTTP request 中使用 Cache-Control,也可以在 HTTP response 中使用它.以一般的商业应用程序而言,用在 HTTP response 的情况相对较多.以 HTTP response 而言,当 HTTP Header 中有 Cache-Control 出现时,代表服务器端能为该网页 (HTTP 里的内容) 来定义它在用户端里的 cache 方式.比较常见的 cache 命令就是 no-cache 和 no-store. No-cache 并非完全限制用户端不 cache 数据,而是让服务器端有弹性来建议用户端是否要用 cache 数据 (还需要其他 HTTP header 一起配合),例如服务器端响应时给 HTTP status code = 304 并且 cache-ctronol 有 no-cache 时,则代表服务器告诉用户端用自己的 cache 数据显示给用户看即可.no-store 就是服务器端告诉用户端,发出 HTTP request 时,此 HTTP request 必须要送到服务器端,不能使用用户端里的 cache 来响应这个 HTTP request.除以之外,其他的命令还有 public, private,only-if-cache 等,这些命令都是用来控制是否能有 cache 的行为.

Public 命令是指网页内容可以放在用户端的 cache 里,如果某一个 HTTP request 所请求的内容在 cache 里并且该内容是 public 时,则用户端就可以直接从 cache 里将数据读出来显示在画面上,而不用将 HTTP request 送到服务器端.

Private 命令是使用 cache 能发生,但只适用在某个使用者中,也就是说这不是一个共用的 cache,而是针对不同的使用者所建立独立的 cache.

Only-if-cached 命令是让旧有的数据产生 cache 行为,既便是服务器端有了新版本的数据,用户端也不会将 Http request 送到服务器端.

这些是用来控制 Cache 行为是否能发生以及发生时该具有什么样的行为.除了这些命令以外,HTTP 1.1 Cache-Control 也定义 cache 具有有效期限的功能.例如 max-age 命令用来定义当数据放到 cache 时何时会过期.假设 max-age=10 代表这份数据能在 cache 中有十秒的合法期限,这代表当用户端在十秒内想要对服务器端发出 HTTP request 要求这份数据时,这 HTTP request 将不会到达服务器端,因为用户端将直接把 cache 里的内容回传.

有关 Cache-Control 所有的内容将记录在 HTTP 1.1 规格里,这篇文章不会把所有命令都说明一遍.如果你觉得 HTTP 1.1 规格较不好懂,建议你可以参考  Mozilla 的 Cache-Control 文档,较为精简易懂.

在 ASP.NET Core MVC 里提供了两种简易的方式来设定 cache,一个是 Cache tag helper,另一个是 ResponseCache attribute.

之前的文章中曾介绍过 tag helper 以及如何撰写 tag helper.ASP.NET Core MVC 内建提供了 Cache tag helper,让开发者可以很简单的将网页内容设定为 cache 内容.接着看一个简单的例子,在 cshtml 中撰写以下的内容

@DateTime.Now

这个时间值将会被暂存二十分钟,也就是说当你发出第一个 Http request 到这网页时,你会看到当时的时间值,而当你再重新发出一个 HTTP request 到相同网页时,你会发现你看到的时间值跟前面是一样的值.默认上 Razor View engine 设定的过期时间为二十分钟后,所以该时间值将被保留二十分钟.

Cache tag helper 还提供其他的属性可以设定,这篇文章不再对其他属性多做介绍.其他的属性数据可以在 Cache tag helper 官方文档中找到.

除了 Cache tag helper 之外,ASP.NET Core MVC 提供了 ResponseCache 属性可以让开发者以 Controller 或是 Controller 的 Action 产生的内容为单位来设定 cache.底下是一个简单的例子,

[ResponseCache(Duration = 60)]
public IActionResult About()
{
	ViewData["Time"] = DateTime.Now.ToString();
}

以上的例子将会把 Time 的过期时间设定在六十秒后,也就是说在六十秒内,你对 About() action 发出 HTTP request 时,你得到的 Time 会是同一个值.当你用浏览器的 F12 工具或是像 POSTMAN 之类的工具来观察 HTTP 连线内容时,你将会看到 HTTP Response header 里的 cache-control 的内容是 public, max-age=60.此时应该能让你了解到 ResponseCache attribute 就是方便让你设定 cache-control 之处了.再来看另外一个例子,

[ResponseCache(Location = ResponseCacheLocation.Client)]
public IActionResult About()
{
	ViewData["Time"] = DateTime.Now.ToString();
}

ResponseCacheLocation 是一个 enum,用来定义 cache 所用的空间所在地.当设定为 Client 时,相当于将 cache-control 设定为 private,等于 cache 的内容不会在浏览器之间共用,自己只能用自己的 cache.有关这属性的细节,你可以参考 ASP.NET Core 的 api 文档.

再来看一个不同的例子,

[ResponseCache(Location = ResponseCacheLocation.Any, NoStore =true )]
public IActionResult About()
{
	ViewData["Time"] = DateTime.Now.ToString();

	return View();
}

你为 cache-control 指定了 no-store,同时也指定了 public (Location = Any),这让 Location 看起来是多余的.

Cache-control 跟 Vary header 也可以一起合作.Vary header 基本上就是用来对内容做 “差异化” 的办别.服务器回传数据给用户端时,可以先回传多个内容选择给用户端,让用户端决定用什么样的内容显示给用户,例如不同语言,不同文档类型,有没有压缩等.不同的用户端不见得能用同一份数据,例如手机用户端和一般桌机用户端所接收到的数据理论上应该有所不同.Vary header 便用来定义这些文档还要依什么条件而来将内容视为不同.很常见的就是 User-Agent.因为手机用户端和桌机用户端使用的浏览器应该要传出不同的 User-Agent 内容.因此,透过 Vary 的内容也能告诉 cache-control 该如何响应 cache 数据,以免 cache-control 回传不适合的 cache 数据.以下是一个使用 Vary = User-Agent 的例子,

[ResponseCache(VaryByHeader = "User-Agent", Duration = 30)]
public IActionResult About()
{
}

VaryByHeader 便是用来设定 Vary header 的内容.除此之外,ResponseCache 属性还能针对 URL 里不同的 query string 来执行 cache 功能,这是透过 VaryByQueryKeys 来执行.如下面的例子,

[ResponseCache(VaryByQueryKeys = new string[]{"key1"}, Duration =60 )]
public IActionResult About()
{
}

当前一个 Http request url 是 http://{server_address}/{controller}/{action}?key1=value1 而且后一个 Http request 也是用相同的 query string (key1=valuye1),则 cache-control 将会有作用.Http request 被服务器收到进入到 middleware 层层处理时,数据将会直接从 Response cache middleware 回传,而不需要再进入到 MVC middleware 来处理.不过这项功能必须搭配 Response cache middleware 才能达成.

下一篇的文章将介绍 Response cache middleware.

Hope it helps,