ASP.NET Core 6框架揭祕範例演示[34]:快取整個響應內容

2022-08-29 09:00:24

我們利用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"