我们利用ASP.NET开发的大部分API都是为了对外提供资源,对于不易变化的资源内容,针对某个维度对其实施缓存可以很好地提供应用的性能。《内存缓存与分布式缓存的使用》介绍的两种缓存框架(本地内存缓存和分布式缓存)为我们提供了简单易用的缓存读写编程模式,本篇介绍的则是针对针对HTTP响应内容实施缓存,ResponseCachingMiddleware中间件赋予我们的能力(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)。

目录
[S2201]基于路径的响应缓存(源代码

[S2202]基于指定的查询字符串缓存响应(源代码

[S2203]基于指定的请求报头缓存响应(源代码

[S2204]缓存屏蔽(源代码

[S2201]基于路径的响应缓存

为了确定响应内容是否被缓存,如下的演示程序针对路径“/{foobar?}”注册的中间件会返回当前的时间。如代码片段所示,我们调用UseResponseCaching扩展方法对ResponseCachingMiddleware中间件进行了注册, AddResponseCaching扩展方法则注册了该中间件依赖的服务。

using Microsoft.Net.Http.Headers;

var app = WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar}", Process);
app.Run(); static DateTimeOffset Process(HttpResponse response)
{
response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(3600)
};
return DateTimeOffset.Now;
}

终结点处理方法Process在返回当前时间之前添加了一个Cache-Control响应报头,并且将它的值设置为“public, max-age=3600”(public表示缓存的是可以被所有用户共享的公共数据,而max-age则表示过期时限,单位为秒)。要证明整个响应的内容是否被缓存,只需要验证在缓存过期之前具有相同路径的多个请求对应的响应是否具有相同的主体内容。

GET http://localhost:5000/foo HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:13:39 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:13:39.8838806+08:00"
GET http://localhost:5000/foo HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:13:39 GMT
Server: Kestrel
Age: 3
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:13:39.8838806+08:00"
GET http://localhost:5000/bar HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:13:49 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:13:49.0153031+08:00"
GET http://localhost:5000/bar HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:13:49 GMT
Server: Kestrel
Age: 2
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:13:49.0153031+08:00"

如下所示的四组请求和响应是在不同时间发送的,其中两个和后两个请求采用的请求路径分别为“/foo”和“/bar”。可以看出采用相同路径的请求会得到相同的时间戳,意味着后续请求返回的内容来源于缓存,并且说明了响应内容默认是基于请求路径进行缓存的。由于请求发送的时间不同,所以返回的缓存副本的“年龄”(对应响应报头Age)也是不同的。

[S2202]基于指定的查询字符串缓存响应

一般来说,对于提供资源的API来说,请求的路径可以作为资源的标识,所以请求路径决定返回的资源,这也是响应基于路径进行缓存的理论依据。但是在很多情况下,请求路径仅仅是返回内容的决定性因素之一,即使路径能够唯一标识返回的资源,但是资源可以采用不同的语言来表达,也可以采用不同的编码方式,所以最终的响应的内容还是不一样的。在编写请求处理程序的时候,我们还经常根据请求携带的查询字符串来生成响应的内容。以我们的演示的返回当前时间戳的实例来说,我们可以利用请求携带的查询字符串“utc”或者请求报头“X-UTC”来决定返回的是本地时间还是UTC时间。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; var app = WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar?}", Process);
app.Run(); static DateTimeOffset Process(HttpResponse response,
[FromHeader(Name = "X-UTC")] string? utcHeader,
[FromQuery(Name ="utc")]string? utcQuery)
{
response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(3600)
}; return Parse(utcHeader) ?? Parse(utcQuery) ?? false
? DateTimeOffset.UtcNow : DateTimeOffset.Now; static bool? Parse(string? value)
=> value == null
? null
: string.Compare(value, "1", true) == 0 || string.Compare(value, "true", true) == 0;
}

由于响应缓存默认采用的Key是派生于请求的路径,但是对于我们修改过的这个程序来说,默认的这个缓存键的生成策略就有问题了。程序启动后,我们采用路径“/foobar”发送了如下两个请求,其中第一个请求返回了实时生成的本地时间(+08:00表示北京时间采用的时区),对于第二个情况下,我们本来希望指定“utc”查询字符串以返回一个UTC时间,但是我们得到却是缓存的本地时间。

GET http://localhost:5000/foobar HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:54:54 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:54:54.6845646+08:00"
GET http://localhost:5000/foobar?utc=true HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:54:54 GMT
Server: Kestrel
Age: 7
Cache-Control: public, max-age=3600
Content-Length: 35 "2021-12-14T10:54:54.6845646+08:00"

[S2203]基于指定的请求报头缓存响应

要解决这个问题,必须要让我们希望的缓存维度作为缓存键的组成部分。就我们演示程序来说,就是得让响应缓存的Key不仅仅包括请求的路径,还应该包括查询字符串“utc”和请求报头“X-UTC”的值。为此我们对演示的程序进行了相应的修改。如下面的代码片段所示,我们从当前HttpContext上下文中提取出IResponseCachingFeature特性,并将设置了它的VaryByQueryKeys属性使之包含了参与缓存的查询字符串的名称“utc”。为了让自定义请求报头“X-UTC”的值也参与缓存,我们将“X-UTC”作为Vary响应报头的值。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.Net.Http.Headers; var app = WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar?}", Process);
app.Run(); static DateTimeOffset Process(HttpContext httpContext,
[FromHeader(Name = "X-UTC")] string? utcHeader,
[FromQuery(Name ="utc")]string? utcQuery)
{
var response = httpContext.Response;
response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromSeconds(3600)
}; var feature = httpContext.Features.Get<IResponseCachingFeature>()!;
feature.VaryByQueryKeys = new string[] { "utc" };
response.Headers.Vary = "X-UTC"; return Parse(utcHeader) ?? Parse(utcQuery) ?? false ? DateTimeOffset.UtcNow : DateTimeOffset.Now; static bool? Parse(string? value)
=> value == null? null: string.Compare(value, "1", true) == 0 || string.Compare(value, "true", true) == 0;
}

对于我们修正过演示程序来说,请求查询字符串“utc”的值会作为响应缓存键的一部分,我们在重启应用后发送了如下针对“/foobar”的四个请求。前两个请求和后两个请求采用相同的查询字符串(“?utc=true”和“?utc=false”),所以后一个请求会返回缓存的内容。

GET http://localhost:5000/foobar?utc=true HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:59:23 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T02:59:23.0540999+00:00"
GET http://localhost:5000/foobar?utc=true HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 02:59:23 GMT
Server: Kestrel
Age: 3
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T02:59:23.0540999+00:00"

从上面给出的报文的内容可以看出,响应报文具有一个值为“X-UTC”的Vary报头,它告诉客户端响应的内容会根据这个名为“X-UTC”的请求报头进行缓存。为了验证这一点,我们在重启应用后针对“/foobar”发送了如下四个请求,前两个请求和后两个请求采用相同的X-UTC(“X-UTC: True”和“X-UTC: False”),所以后一个请求会返回缓存的内容。

GET http://localhost:5000/foobar HTTP/1.1
X-UTC: True
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:05:06 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 34 "2021-12-14T03:05:06.977078+00:00"
GET http://localhost:5000/foobar HTTP/1.1
X-UTC: True
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:05:06 GMT
Server: Kestrel
Age: 3
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 34 "2021-12-14T03:05:06.977078+00:00"
GET http://localhost:5000/foobar HTTP/1.1
X-UTC: False
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:05:17 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:05:17.0068036+08:00"
GET http://localhost:5000/foobar HTTP/1.1
X-UTC: False
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:05:17 GMT
Server: Kestrel
Age: 19
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:05:17.0068036+08:00"

[S2204]缓存屏蔽

响应缓存通过复用已经生成的响应内容来提升性能,但不意味任何请求都适合以缓存的内容予以回复,请求携带的一些报头会屏蔽掉响应缓存。或者更加准确的说法是,客户端请求携带的一些报头会“提醒”服务端当前场景需要返回实时内容。比如携带Authorization报头的请求默认情况下将不会使用缓存的内容予以回复,下面的请求/响应体现了这一点。

GET http://localhost:5000/foobar HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:13:10 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:13:10.4605924+08:00"
GET http://localhost:5000/foobar HTTP/1.1
Authorization: foobar
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:13:17 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:13:18.0918033+08:00"

关于Authorization请求报头与缓存的关系,它与前面介绍的根据指定的请求报头对响应内容进行缓存是不一样的,当ResponseCachingMiddleware中间件在处理请求时,只要请求携带了此报头,缓存策略将不再使用。如果客户端对数据的实时性要求很高,那么它更希望服务总是返回实时生成的内容,这种情况下它利用利用携带的一些请求报头向服务端传达这样的意图,此时一般会使用到报头“Cache-Control:no-cache”或者“Pragma:no-cache”。这两个请求报头对响应缓存的屏蔽作用体现在如下所示的四组请求/响应中。

GET http://localhost:5000/foobar HTTP/1.1
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:15:16 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 34 "2021-12-14T11:15:16.423496+08:00"
GET http://localhost:5000/foobar HTTP/1.1
Cache-Control: no-cache
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:15:26 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:15:26.7701298+08:00"
GET http://localhost:5000/foobar HTTP/1.1
Pragma: no-cache
Host: localhost:5000 HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 14 Dec 2021 03:15:36 GMT
Server: Kestrel
Cache-Control: public, max-age=3600
Vary: X-UTC
Content-Length: 35 "2021-12-14T11:15:36.5283536+08:00"

ASP.NET Core 6框架揭秘实例演示[34]:缓存整个响应内容的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. 探索链路追踪在.NET6工业物联网项目的应用

    ExploringIoTDistributedTracingNet6 如果觉得有用,请留言学到了. 已经会了的老哥,请留言就这? 可能遇到的问题 工业物联网项目自上而下一般分为ERP.Mes.SCAD ...

  2. 基于Svelte3.x桌面端UI组件库Svelte UI

    Svelte-UI,一套基于svelte.js开发的桌面pc端ui组件库 最近一直忙于写svelte-ui,一套svelte3开发的桌面端ui组件库.在设计及功能上借鉴了element-ui组件库.所 ...

  3. browserify的standalone的含义

    白话:就像Jquery的$, 把你打包后的函数挂在window下你指定的名字下 废话:白话看不懂,就看下面的废话,你不得不花更多时间理解--standalone AAA的含义$ browserify ...

  4. windows配置skywalking集群

    一.zookeeper 准备配置三个zookeeper,因为我是单台模拟,所以需要使用不同的端口,使用版本是apache-zookeeper-3.6.3-bin (必须是3.5+) 1.第1个zook ...

  5. 05 MySQL_主键约束

    主键约束 主键: 用于表示数据唯一性的字段称为主键: 约束:就是对表字段添加限制条件 主键约束:保证主键字段的值唯一且非空: - 格式 : create table t1(id int primary ...

  6. 聊一聊 C# 后台GC 到底是怎么回事?

    一:背景 写这一篇的目的主要是因为.NET领域内几本关于阐述GC方面的书,都是纯理论,所以懂得人自然懂,不懂得人也没法亲自验证,这一篇我就用 windbg + 源码 让大家眼见为实. 二:为什么要引入 ...

  7. Harbor企业级私服Docker镜像仓库搭建及应用

    一.简介 Docker Hub作为Docker默认官方公共镜像,如果想要自己搭建私有镜像,Harbor是企业级镜像库非常好的选择. 所谓私有仓库,也就是在本地(局域网)搭建的一个类似公共仓库的东西,搭 ...

  8. ROS机械臂 Movelt 学习笔记3 | kinect360相机(v1)相关配置

    目标是做一个机械臂视觉抓取的demo,在基地里翻箱倒柜,没有找到学长所说的 d435,倒是找到了一个老古董 kinect 360. 前几天就已经在旧电脑上配置好了,现在记录在新电脑上的配置过程. 1. ...

  9. 第四天python3 python解析式-生成器-迭代器

    标准库datetime datetime模块  对日期.时间.时间戳的处理 datetime类 类方法: today() 返回本地时区当前时间的datetime对象: now(tz=None) 返回当 ...

  10. impl和dyn用法

    先推荐一个链接 理解 Rust 2018 edition 的两个新关键字 -- impl 和 dyn 官方文档中,impl是限定泛型的语法糖,所以 trait Trait {} fn foo<T ...