前言

前面的博客谈的大多数都是针对数据的缓存,今天我们来换换口味。来谈谈在ASP.NET Core中的ResponseCaching,与ResponseCaching关联密切的也就是常说的HTTP缓存。

在阅读本文内容之前,默认各位有HTTP缓存相关的基础,主要是Cache-Control相关的。

这里也贴两篇相关的博客:

回到正题,对于ASP.NET Core中的ResponseCaching,本文主要讲三个相关的小内容

  1. 客户端(浏览器)缓存
  2. 服务端缓存
  3. 静态文件缓存

客户端(浏览器)缓存

这里主要是通过设置HTTP的响应头来完成这件事的。方法主要有两种:

其一,直接用Response对象去设置。

这种方式也有两种写法,示例代码如下:

  1. public IActionResult Index()
  2. {
  3. //直接一,简单粗暴,不要拼写错了就好~~
  4. Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] = "public, max-age=600";
  5. //直接二,略微优雅点
  6. //Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
  7. //{
  8. // Public = true,
  9. // MaxAge = TimeSpan.FromSeconds(600)
  10. //};
  11. return View();
  12. }

这两者效果是一样的,大致如下:

它们都会给响应头加上 Cache-Control: public, max-age=600,可能有人会问,加上这个有什么用?

那我们再来看张动图,应该会清晰不少。

这里事先在代码里面设置了一个断点,正常情况下,只要请求这个action都是会进来的。

但是从上图可以发现,只是第一次才进了断点,其他直接打开的都没有进,而是直接返回结果给我们了,这也就说明缓存起作用了。

同样的,再来看看下面的图,from disk cache也足以说明,它并没有请求到服务器,而是直接从本地返回的结果。

注:如果是刷新的话,还是会进断点的。这里需要区分好刷新,地址栏回车等行为。不同浏览器也有些许差异,这里可以用fiddler和postman来模拟。

在上面的做法中,我们将设置头部信息的代码和业务代码混在一起了,这显然不那么合适。

下面来看看第二种方法,也是比较推荐的方法。

其二,用ResponseCacheAttribute去处理缓存相关的事情。

对于和上面的同等配置,只需要下面这样简单设置一个属性就可以了。

  1. [ResponseCache(Duration = 600)]
  2. public IActionResult Index()
  3. {
  4. return View();
  5. }

效果和上面是一致的!处理起来是不是简单多了。

既然这两种方式都能完成一样的效果,那么ResponseCache这个Attribute本质也是往响应头写了相应的值。

但是我们知道,纯粹的Attribute并不能完成这一操作,其中肯定另有玄机!

翻了一下源码,可以看到它实现了IFilterFactory这个关键的接口。

  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  2. public class ResponseCacheAttribute : Attribute, IFilterFactory, IOrderedFilter
  3. {
  4. public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
  5. {
  6. //..
  7. return new ResponseCacheFilter(new CacheProfile
  8. {
  9. Duration = _duration,
  10. Location = _location,
  11. NoStore = _noStore,
  12. VaryByHeader = VaryByHeader,
  13. VaryByQueryKeys = VaryByQueryKeys,
  14. });
  15. }
  16. }

也就是说,真正起作用的是ResponseCacheFilter这个Filter,核心代码如下:

  1. public void OnActionExecuting(ActionExecutingContext context)
  2. {
  3. var headers = context.HttpContext.Response.Headers;
  4. // Clear all headers
  5. headers.Remove(HeaderNames.Vary);
  6. headers.Remove(HeaderNames.CacheControl);
  7. headers.Remove(HeaderNames.Pragma);
  8. if (!string.IsNullOrEmpty(VaryByHeader))
  9. {
  10. headers[HeaderNames.Vary] = VaryByHeader;
  11. }
  12. if (NoStore)
  13. {
  14. headers[HeaderNames.CacheControl] = "no-store";
  15. // Cache-control: no-store, no-cache is valid.
  16. if (Location == ResponseCacheLocation.None)
  17. {
  18. headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache");
  19. headers[HeaderNames.Pragma] = "no-cache";
  20. }
  21. }
  22. else
  23. {
  24. headers[HeaderNames.CacheControl] = cacheControlValue;
  25. }
  26. }

它的本质自然就是给响应头部写了一些东西。

通过上面的例子已经知道了ResponseCacheAttribute运作的基本原理,下面再来看看如何配置出其他不同的效果。

下面的表格列出了部分常用的设置和生成的响应头信息。

ResponseCache的设置 响应头
[ResponseCache(Duration = 600, Location = ResponseCacheLocation.Client)] Cache-Control: private, max-age=600
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] Cache-Control:no-cache, no-store
[ResponseCache(Duration = 60, VaryByHeader = "User-Agent")] Cache-Control : public, max-age=60
Vary : User-Agent

注:如果NoStore没有设置成true,则Duration必须要赋值!

关于ResponseCacheAttribute,还有一个不得不提的属性:CacheProfileName

它相当于指定了一个“配置文件”,并在这个“配置文件”中设置了ResponseCache的一些值。

这个时候,只需要在ResponseCacheAttribute上面指定这个“配置文件”的名字就可以了,而不用在给Duration等属性赋值了。

在添加MVC这个中间件的时候就需要把这些“配置文件”准备好!

下面的示例代码添加了两份“配置文件”,其中一份名为default,默认是缓存10分钟,还有一份名为Hourly,默认是缓存一个小时,还有一些其他可选配置也用注释的方式列了出来。

  1. services.AddMvc(options =>
  2. {
  3. options.CacheProfiles.Add("default", new Microsoft.AspNetCore.Mvc.CacheProfile
  4. {
  5. Duration = 600, // 10 min
  6. });
  7. options.CacheProfiles.Add("Hourly", new Microsoft.AspNetCore.Mvc.CacheProfile
  8. {
  9. Duration = 60 * 60, // 1 hour
  10. //Location = Microsoft.AspNetCore.Mvc.ResponseCacheLocation.Any,
  11. //NoStore = true,
  12. //VaryByHeader = "User-Agent",
  13. //VaryByQueryKeys = new string[] { "aaa" }
  14. });
  15. });

现在“配置文件”已经有了,下面就是使用这些配置了!只需要在Attribute上面指定CacheProfileName的名字就可以了。

示例代码如下:

  1. [ResponseCache(CacheProfileName = "default")]
  2. public IActionResult Index()
  3. {
  4. return View();
  5. }

ResponseCacheAttribute中还有一个VaryByQueryKeys的属性,这个属性可以根据不同的查询参数进行缓存!

但是这个属性的使用需要结合下一小节的内容,所以这里就不展开了。

注:ResponseCacheAttribute即可以加在类上面,也可以加在方法上面,如果类和方法都加了,会优先采用方法上面的配置。

服务端缓存

先简单解释一下这里的服务端缓存是什么,对比前面的客户端缓存,它是将东西存放在客户端,要用的时候就直接从客户端去取!

同理,服务端缓存就是将东西存放在服务端,要用的时候就从服务端去取。

需要注意的是,如果服务端的缓存命中了,那么它是直接返回结果的,也是不会去访问Action里面的内容!有点类似代理的感觉。

这个相比客户端缓存有一个好处,在一定时间内,“刷新”页面的时候会从这里的缓存返回结果,而不用再次访问Action去拿结果。

要想启用服务端缓存,需要在管道中去注册这个服务,核心代码就是下面的两句。

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddResponseCaching();
  4. }
  5. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  6. {
  7. app.UseResponseCaching();
  8. }

当然,仅有这两句代码,并不能完成这里提到的服务端缓存。还需要前面客户端缓存的设置,两者结合起来才能起作用。

可以看看下面的效果,

简单解释一下这张图,

  1. 第一次刷新的时候,会进入中间件,然后进入Action,返回结果,Fiddler记录到了这一次的请求
  2. 第二次打开新标签页,直接从浏览器缓存中返回的结果,即没有进入中间件,也没有进入Action,Fiddler也没有记录到相关请求
  3. 第三次换了一个浏览器,会进入中间件,直接由缓存返回结果,并没有进入Action,此时Fiddler也将该请求记录了下来,响应头包含了Age

第三次请求响应头部的部分信息如下:

  1. Age: 16
  2. Cache-Control: public,max-age=600

这个Age是在变化的!它就等价于缓存的寿命。

如果启用了日志,也会看到一些比较重要的日记信息。

在上一小节中,我们还有提到ResponseCacheAttribute中的VaryByQueryKeys这个属性,它需要结合ResponseCaching中间件一起用的,这点在注释中也是可以看到的!

  1. //
  2. // Summary:
  3. // Gets or sets the query keys to vary by.
  4. //
  5. // Remarks:
  6. // Microsoft.AspNetCore.Mvc.ResponseCacheAttribute.VaryByQueryKeys requires the
  7. // response cache middleware.
  8. public string[] VaryByQueryKeys { get; set; }

举个例子(不一定很合适)来看看,假设现在有一个电影列表页面(http://localhost:5001),可以通过在URL地址上面加查询参数来决定显示第几页的数据。

如果代码是这样写的,

  1. [ResponseCache(Duration = 600)]
  2. public IActionResult List(int page = 0)
  3. {
  4. return Content(page.ToString());
  5. }

结果就会像下面这样,三次请求,返回的都是页码为0的结果!page参数,压根就没起作用!

  1. GET http://localhost:5001/Home/List HTTP/1.1
  2. Host: localhost:5001
  3. HTTP/1.1 200 OK
  4. Date: Thu, 05 Apr 2018 07:38:51 GMT
  5. Content-Type: text/plain; charset=utf-8
  6. Server: Kestrel
  7. Content-Length: 1
  8. Cache-Control: public,max-age=600
  9. 0
  10. GET http://localhost:5001/Home/List?page=2 HTTP/1.1
  11. Host: localhost:5001
  12. HTTP/1.1 200 OK
  13. Date: Thu, 05 Apr 2018 07:38:51 GMT
  14. Content-Type: text/plain; charset=utf-8
  15. Server: Kestrel
  16. Content-Length: 1
  17. Cache-Control: public,max-age=600
  18. Age: 5
  19. 0
  20. GET http://localhost:5001/Home/List?page=5 HTTP/1.1
  21. Host: localhost:5001
  22. HTTP/1.1 200 OK
  23. Date: Thu, 05 Apr 2018 07:38:51 GMT
  24. Content-Type: text/plain; charset=utf-8
  25. Server: Kestrel
  26. Content-Length: 1
  27. Cache-Control: public,max-age=600
  28. Age: 8
  29. 0

正确的做法应该是要指定VaryByQueryKeys,如下所示:

  1. [ResponseCache(Duration = 600, VaryByQueryKeys = new string[] { "page" })]
  2. public IActionResult List(int page = 0)
  3. {
  4. return Content(page.ToString());
  5. }

这个时候的结果就是和预期的一样了,不同参数都有对应的结果并且这些数据都缓存了起来。

  1. GET http://localhost:5001/Home/List HTTP/1.1
  2. Host: localhost:5001
  3. HTTP/1.1 200 OK
  4. Date: Thu, 05 Apr 2018 07:45:13 GMT
  5. Content-Type: text/plain; charset=utf-8
  6. Server: Kestrel
  7. Content-Length: 1
  8. Cache-Control: public,max-age=600
  9. 0
  10. GET http://localhost:5001/Home/List?page=2 HTTP/1.1
  11. Host: localhost:5001
  12. HTTP/1.1 200 OK
  13. Date: Thu, 05 Apr 2018 07:45:22 GMT
  14. Content-Type: text/plain; charset=utf-8
  15. Server: Kestrel
  16. Content-Length: 1
  17. Cache-Control: public,max-age=600
  18. 2
  19. GET http://localhost:5001/Home/List?page=5 HTTP/1.1
  20. Host: localhost:5001
  21. HTTP/1.1 200 OK
  22. Date: Thu, 05 Apr 2018 07:45:27 GMT
  23. Content-Type: text/plain; charset=utf-8
  24. Server: Kestrel
  25. Content-Length: 1
  26. Cache-Control: public,max-age=600
  27. 5

ResponseCachingMiddleware在这里是用了MemoryCache来读写缓存数据的。如果应用重启了,缓存的数据就会失效,要重新来过。

静态文件缓存

对于一些常年不变或比较少变的js,css等静态文件,也可以把它们缓存起来,避免让它们总是发起请求到服务器,而且这些静态文件可以缓存更长的时间!

如果已经使用了CDN,这一小节的内容就可以暂且忽略掉了。。。

对于静态文件,.NET Core有一个单独的StaticFiles中间件,如果想要对它做一些处理,同样需要在管道中进行注册。

UseStaticFiles有几个重载方法,这里用的是带StaticFileOptions参数的那个方法。

因为StaticFileOptions里面有一个OnPrepareResponse可以让我们修改响应头,以达到HTTP缓存的效果。

  1. //
  2. // Summary:
  3. // Called after the status code and headers have been set, but before the body has
  4. // been written. This can be used to add or change the response headers.
  5. public Action<StaticFileResponseContext> OnPrepareResponse { get; set; }

下面来看个简单的例子:

  1. app.UseStaticFiles(new StaticFileOptions
  2. {
  3. OnPrepareResponse = context =>
  4. {
  5. context.Context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
  6. {
  7. Public = true,
  8. //for 1 year
  9. MaxAge = System.TimeSpan.FromDays(365)
  10. };
  11. }
  12. });

此时的效果如下:

一些需要注意的地方

其一,ResponseCaching中间件对下面的情况是不会进行缓存操作的!

  1. 一个请求的Status Code不是200
  2. 一个请求的Method不是GETHEAD
  3. 一个请求的Header包含Authorization
  4. 一个请求的Header包含Set-Cookie
  5. 一个请求的Header包含仅有值为*的Vary
  6. ...

其二,当我们使用了Antiforgery的时候也要特别的注意!!它会直接把响应头部的Cache-ControlPragma重置成no-cache。换句话说,这两者是水火不容的!

详情可见DefaultAntiforgery.cs#L381

  1. /// <summary>
  2. /// Sets the 'Cache-Control' header to 'no-cache, no-store' and 'Pragma' header to 'no-cache' overriding any user set value.
  3. /// </summary>
  4. /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
  5. protected virtual void SetDoNotCacheHeaders(HttpContext httpContext)
  6. {
  7. // Since antifogery token generation is not very obvious to the end users (ex: MVC's form tag generates them
  8. // by default), log a warning to let users know of the change in behavior to any cache headers they might
  9. // have set explicitly.
  10. LogCacheHeaderOverrideWarning(httpContext.Response);
  11. httpContext.Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
  12. httpContext.Response.Headers[HeaderNames.Pragma] = "no-cache";
  13. }

当然,在某个页面用到了Antiforgery的时候,也该避免在这个页面使用HTTP缓存!

它会在form表单中生成一个隐藏域,并且隐藏域的值是一个生成的token ,难道还想连这个一起缓存?

总结

在.NET Core中用ResponseCaching还是比较简单的,虽然还有一些值得注意的地方,但是并不影响我们的正常使用。

当然,最重要的还是合理使用!仅在需要的地方使用!

最后附上文中Demo的地址

ResponseCachingDemo

谈谈ASP.NET Core中的ResponseCaching的更多相关文章

  1. ASP.NET Core 中的那些认证中间件及一些重要知识点

    前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...

  2. Asp.net Core中使用Session

    前言 2017年就这么悄无声息的开始了,2017年对我来说又是特别重要的一年. 元旦放假在家写了个Asp.net Core验证码登录, 做demo的过程中遇到两个小问题,第一是在Asp.net Cor ...

  3. 在ASP.NET Core中使用百度在线编辑器UEditor

    在ASP.NET Core中使用百度在线编辑器UEditor 0x00 起因 最近需要一个在线编辑器,之前听人说过百度的UEditor不错,去官网下了一个.不过服务端只有ASP.NET版的,如果是为了 ...

  4. ASP.NET Core中的依赖注入(1):控制反转(IoC)

    ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...

  5. ASP.NET Core中的依赖注入(2):依赖注入(DI)

    IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用"好莱坞原则"是应用程序以被动的方式实现对流程的定制.我们可以采用若干设计 ...

  6. ASP.NET Core中的依赖注入(3): 服务的注册与提供

    在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core ...

  7. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  8. ASP.NET Core 中文文档 第二章 指南(4.6)Controller 方法与视图

    原文:Controller methods and views 作者:Rick Anderson 翻译:谢炀(Kiler) 校对:孟帅洋(书缘) .张仁建(第二年.夏) .许登洋(Seay) .姚阿勇 ...

  9. ASP.NET Core 中文文档 第三章 原理(1)应用程序启动

    原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...

随机推荐

  1. Redis之PHP操作

    一.Redis连接与认证 //连接参数:ip.端口.连接超时时间,连接成功返回true,否则返回false $ret = $redis->connect('127.0.0.1', 6379, 3 ...

  2. 28.Django cookie

    概述 1.获取cookie request.COOKIES['key'] request.COOKIES.get('key') request.get_signed_cookie(key, defau ...

  3. Oracle GoldenGate实现数据库同步

    前言:最近刚好在弄数据库同步,网上查了些资料再加上自己整理了一些,做个分享! 一.GoldenGate的安装 1.安装包准备 数据库版本:Oracle Database 11g Release 2(1 ...

  4. ajax提交表单、ajax实现文件上传

    ajax提交表单.ajax实现文件上传,有需要的朋友可以参考下. 方式一:利用from表单的targer属性 + 隐藏的iframe 达到类似效果, 支持提交含有文件和普通数据的复杂表单 方式二:使用 ...

  5. secureCRT的安装及破解

    secureCRT是我们平时都会用到的终端仿真程序,所谓是居家旅行必备神器啊,下面就说说怎么安装破解secureCRT. (网上有破解版和一些绿色版,感觉或多或少都有点问题,比如我用便携版就有问题,所 ...

  6. IPFS家族(一)

    IPFS这个项目其实很大,并不像大家想象的是一个东西,IPFS是由很多模块组成,每一个模块现在都已经独立成项目了,并且有自己的主页.让我们来简单看一下IPFS家族成员. 协议实验室的主页:https: ...

  7. go语言defer panic recover用法总结

    defer defer是go提供的一种资源处理的方式.defer的用法遵循3个原则 在defer表达式被运算的同时,defer函数的参数也会被运算.如下defer的表达式println运算的同时,其入 ...

  8. 堆排序(Java数组实现)

    堆排序:利用大根堆 数组全部入堆,再出堆从后向前插入回数组中,数组就从小到大有序了. public class MaxHeap<T extends Comparable<? super T ...

  9. java报错排解

    1.eclipse新安装第一次启动报错: Javawas started but returned exit code=13-- 这是由于JDK和eclipse和电脑的位数不一致所致,要么都为32位, ...

  10. 笔记:Hibernate 数据库方言表

    关系数据库 方言 DB2 org.hibernate.dialect.DB2Dialect DB2 AS/400 org.hibernate.dialect.DB2400Dialect DB2 OS/ ...