談談ASP.NET Core中的ResponseCaching
前言
前面的博客談的大多數都是針對數據的緩存,今天我們來換換口味。來談談在ASP.NET Core中的ResponseCaching,與ResponseCaching關聯密切的也就是常說的HTTP緩存。
在閱讀本文內容之前,默認各位有HTTP緩存相關的基礎,主要是Cache-Control相關的。
這裡也貼兩篇相關的博客:
透過瀏覽器看HTTP緩存
HTTP協議 (四) 緩存
回到正題,對於ASP.NET Core中的ResponseCaching,本文主要講三個相關的小內容
客戶端(瀏覽器)緩存
服務端緩存
靜態文件緩存
客戶端(瀏覽器)緩存
這裡主要是通過設置HTTP的響應頭來完成這件事的。方法主要有兩種:
其一,直接用Response對象去設置。
這種方式也有兩種寫法,示例代碼如下:
public IActionResult Index()
{
//直接一,簡單粗暴,不要拼寫錯了就好~~
Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] = "public, max-age=600";
//直接二,略微優雅點
//Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
//{
// Public = true,
// MaxAge = TimeSpan.FromSeconds(600)
//};
return View();
}
這兩者效果是一樣的,大致如下:
它們都會給響應頭加上 ,可能有人會問,加上這個有什麼用?
那我們再來看張動圖,應該會清晰不少。
GIF
這裡事先在代碼裡面設置了一個斷點,正常情況下,只要請求這個action都是會進來的。
但是從上圖可以發現,只是第一次才進了斷點,其他直接打開的都沒有進,而是直接返回結果給我們了,這也就說明緩存起作用了。
同樣的,再來看看下面的圖,也足以說明,它並沒有請求到伺服器,而是直接從本地返回的結果。
註:如果是刷新的話,還是會進斷點的。這裡需要區分好刷新,地址欄回車等行為。不同瀏覽器也有些許差異,這裡可以用fiddler和postman來模擬。
在上面的做法中,我們將設置頭部信息的代碼和業務代碼混在一起了,這顯然不那麼合適。
下面來看看第二種方法,也是比較推薦的方法。
其二,用ResponseCacheAttribute去處理緩存相關的事情。
對於和上面的同等配置,只需要下面這樣簡單設置一個屬性就可以了。
效果和上面是一致的!處理起來是不是簡單多了。
既然這兩種方式都能完成一樣的效果,那麼ResponseCache這個Attribute本質也是往響應頭寫了相應的值。
但是我們知道,純粹的Attribute並不能完成這一操作,其中肯定另有玄機!
翻了一下源碼,可以看到它實現了IFilterFactory這個關鍵的介面。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ResponseCacheAttribute : Attribute, IFilterFactory, IOrderedFilter
{
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
//..
return new ResponseCacheFilter(new CacheProfile
{
Duration = _duration,
Location = _location,
NoStore = _noStore,
VaryByHeader = VaryByHeader,
VaryByQueryKeys = VaryByQueryKeys,
});
}
}
也就是說,真正起作用的是ResponseCacheFilter這個Filter,核心代碼如下:
public void OnActionExecuting(ActionExecutingContext context)
{
var headers = context.HttpContext.Response.Headers;
// Clear all headers
headers.Remove(HeaderNames.Vary);
headers.Remove(HeaderNames.CacheControl);
headers.Remove(HeaderNames.Pragma);
if (!string.IsNullOrEmpty(VaryByHeader))
{
headers[HeaderNames.Vary] = VaryByHeader;
}
if (NoStore)
{
headers[HeaderNames.CacheControl] = "no-store";
// Cache-control: no-store, no-cache is valid.
if (Location == ResponseCacheLocation.None)
{
headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache");
headers[HeaderNames.Pragma] = "no-cache";
}
}
else
{
headers[HeaderNames.CacheControl] = cacheControlValue;
}
}
它的本質自然就是給響應頭部寫了一些東西。
通過上面的例子已經知道了ResponseCacheAttribute運作的基本原理,下面再來看看如何配置出其他不同的效果。
下面的表格列出了部分常用的設置和生成的響應頭信息。
註:如果NoStore沒有設置成true,則Duration必須要賦值!
關於ResponseCacheAttribute,還有一個不得不提的屬性:CacheProfileName!
它相當於指定了一個「配置文件」,並在這個「配置文件」中設置了ResponseCache的一些值。
這個時候,只需要在ResponseCacheAttribute上面指定這個「配置文件」的名字就可以了,而不用在給Duration等屬性賦值了。
在添加MVC這個中間件的時候就需要把這些「配置文件」準備好!
下面的示例代碼添加了兩份「配置文件」,其中一份名為default,默認是緩存10分鐘,還有一份名為Hourly,默認是緩存一個小時,還有一些其他可選配置也用注釋的方式列了出來。
services.AddMvc(options =>
{
options.CacheProfiles.Add("default", new Microsoft.AspNetCore.Mvc.CacheProfile
{
Duration = 600, // 10 min
});
options.CacheProfiles.Add("Hourly", new Microsoft.AspNetCore.Mvc.CacheProfile
{
Duration = 60 * 60, // 1 hour
//Location = Microsoft.AspNetCore.Mvc.ResponseCacheLocation.Any,
//NoStore = true,
//VaryByHeader = "User-Agent",
//VaryByQueryKeys = new string[] { "aaa" }
});
});
現在「配置文件」已經有了,下面就是使用這些配置了!只需要在Attribute上面指定CacheProfileName的名字就可以了。
示例代碼如下:
[ResponseCache(CacheProfileName = "default")]
public IActionResult Index()
{
return View();
}
ResponseCacheAttribute中還有一個VaryByQueryKeys的屬性,這個屬性可以根據不同的查詢參數進行緩存!
但是這個屬性的使用需要結合下一小節的內容,所以這裡就不展開了。
註:ResponseCacheAttribute即可以加在類上面,也可以加在方法上面,如果類和方法都加了,會優先採用方法上面的配置。
服務端緩存
先簡單解釋一下這裡的服務端緩存是什麼,對比前面的客戶端緩存,它是將東西存放在客戶端,要用的時候就直接從客戶端去取!
同理,服務端緩存就是將東西存放在服務端,要用的時候就從服務端去取。
需要注意的是,如果服務端的緩存命中了,那麼它是直接返回結果的,也是不會去訪問Action裡面的內容!有點類似代理的感覺。
這個相比客戶端緩存有一個好處,在一定時間內,「刷新」頁面的時候會從這裡的緩存返回結果,而不用再次訪問Action去拿結果。
要想啟用服務端緩存,需要在管道中去註冊這個服務,核心代碼就是下面的兩句。
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCaching();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseResponseCaching();
}
當然,僅有這兩句代碼,並不能完成這裡提到的服務端緩存。還需要前面客戶端緩存的設置,兩者結合起來才能起作用。
可以看看下面的效果,
GIF
簡單解釋一下這張圖,
第一次刷新的時候,會進入中間件,然後進入Action,返回結果,Fiddler記錄到了這一次的請求
第二次打開新標籤頁,直接從瀏覽器緩存中返回的結果,即沒有進入中間件,也沒有進入Action,Fiddler也沒有記錄到相關請求
第三次換了一個瀏覽器,會進入中間件,直接由緩存返回結果,並沒有進入Action,此時Fiddler也將該請求記錄了下來,響應頭包含了Age
第三次請求響應頭部的部分信息如下:
這個Age是在變化的!它就等價於緩存的壽命。
如果啟用了日誌,也會看到一些比較重要的日記信息。
在上一小節中,我們還有提到ResponseCacheAttribute中的VaryByQueryKeys這個屬性,它需要結合ResponseCaching中間件一起用的,這點在注釋中也是可以看到的!
//
// Summary:
// Gets or sets the query keys to vary by.
//
// Remarks:
// Microsoft.AspNetCore.Mvc.ResponseCacheAttribute.VaryByQueryKeys requires the
// response cache middleware.
public string[] VaryByQueryKeys { get; set; }
舉個例子(不一定很合適)來看看,假設現在有一個電影列表頁面(http://localhost:5001),可以通過在URL地址上面加查詢參數來決定顯示第幾頁的數據。
如果代碼是這樣寫的,
[ResponseCache(Duration = 600)]
public IActionResult List(int page = 0)
{
return Content(page.ToString());
}
結果就會像下面這樣,三次請求,返回的都是頁碼為0的結果!page參數,壓根就沒起作用!
GET http://localhost:5001/Home/List HTTP/1.1
Host: localhost:5001
HTTP/1.1 200 OK
Date: Thu, 05 Apr 2018 07:38:51 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Content-Length: 1
Cache-Control: public,max-age=600
GET http://localhost:5001/Home/List?page=2 HTTP/1.1
Host: localhost:5001
HTTP/1.1 200 OK
Date: Thu, 05 Apr 2018 07:38:51 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Content-Length: 1
Cache-Control: public,max-age=600
Age: 5
GET http://localhost:5001/Home/List?page=5 HTTP/1.1
Host: localhost:5001
HTTP/1.1 200 OK
Date: Thu, 05 Apr 2018 07:38:51 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Content-Length: 1
Cache-Control: public,max-age=600
Age: 8
正確的做法應該是要指定VaryByQueryKeys,如下所示:
[ResponseCache(Duration = 600, VaryByQueryKeys = new string[] { "page" })]
public IActionResult List(int page = 0)
{
return Content(page.ToString());
}
這個時候的結果就是和預期的一樣了,不同參數都有對應的結果並且這些數據都緩存了起來。
GEThttp://localhost:5001/Home/ListHTTP/1.1Host: localhost:5001HTTP/1.1 200 OKDate: Thu, 05 Apr 2018 07:45:13 GMTContent-Type: text/plain; charset=utf-8Server: KestrelContent-Length: 1Cache-Control:public,max-age=600GEThttp://localhost:5001/Home/List?page=2HTTP/1.1Host: localhost:5001HTTP/1.1200OKDate: Thu,05Apr201807:45:22GMTContent-Type:text/plain; charset=utf-8Server: KestrelContent-Length: 1Cache-Control:public,max-age=6002GEThttp://localhost:5001/Home/List?page=5HTTP/1.1Host: localhost:5001HTTP/1.1200OKDate: Thu,05Apr201807:45:27GMTContent-Type:text/plain; charset=utf-8Server: KestrelContent-Length: 1Cache-Control:public,max-age=6005
ResponseCachingMiddleware在這裡是用了MemoryCache來讀寫緩存數據的。如果應用重啟了,緩存的數據就會失效,要重新來過。
靜態文件緩存
對於一些常年不變或比較少變的js,css等靜態文件,也可以把它們緩存起來,避免讓它們總是發起請求到伺服器,而且這些靜態文件可以緩存更長的時間!
如果已經使用了CDN,這一小節的內容就可以暫且忽略掉了。。。
對於靜態文件,.NET Core有一個單獨的StaticFiles中間件,如果想要對它做一些處理,同樣需要在管道中進行註冊。
有幾個重載方法,這裡用的是帶StaticFileOptions參數的那個方法。
因為StaticFileOptions裡面有一個OnPrepareResponse可以讓我們修改響應頭,以達到HTTP緩存的效果。
//
// Summary:
// Called after the status code and headers have been set, but before the body has
// been written. This can be used to add or change the response headers.
public Action OnPrepareResponse { get; set; }
下面來看個簡單的例子:
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = context =>
{
context.Context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
//for 1 year
MaxAge = System.TimeSpan.FromDays(365)
};
}
});
此時的效果如下:
一些需要注意的地方
其一,ResponseCaching中間件對下面的情況是不會進行緩存操作的!
一個請求的Status Code不是200
一個請求的Method不是GET或HEAD
一個請求的Header包含Authorization
一個請求的Header包含Set-Cookie
一個請求的Header包含僅有值為*的Vary
...
其二,當我們使用了Antiforgery的時候也要特別的注意!!它會直接把響應頭部的Cache-Control和Pragma重置成no-cache。換句話說,這兩者是水火不容的!
詳情可見DefaultAntiforgery.cs#L381
///
/// Sets the "Cache-Control" header to "no-cache, no-store" and "Pragma" header to "no-cache" overriding any user set value.
///
///
The .
protected virtual void SetDoNotCacheHeaders(HttpContext httpContext)
{
// Since antifogery token generation is not very obvious to the end users (ex: MVC"s form tag generates them
// by default), log a warning to let users know of the change in behavior to any cache headers they might
// have set explicitly.
LogCacheHeaderOverrideWarning(httpContext.Response);
httpContext.Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
httpContext.Response.Headers[HeaderNames.Pragma] = "no-cache";
}
當然,在某個頁面用到了Antiforgery的時候,也該避免在這個頁面使用HTTP緩存!
它會在form表單中生成一個隱藏域,並且隱藏域的值是一個生成的token ,難道還想連這個一起緩存?
總結
在.NET Core中用ResponseCaching還是比較簡單的,雖然還有一些值得注意的地方,但是並不影響我們的正常使用。
當然,最重要的還是合理使用!僅在需要的地方使用!
最後附上文中Demo的地址 :https://github.com/catcherwong/Demos/tree/master/src/ResponseCachingDemo
原文地址 https://www.cnblogs.com/catcher1994/p/responsecaching.html
※開源服務容錯處理庫Polly使用文檔
※ASP.NET Core MVC 2.1 頂級參數驗證
TAG:dotNET跨平台 |