客户端(浏览器)缓存
通过设置HTTP的响应头来完成
1、直接用Response对象去设置
[HttpGet] public IEnumerable<WeatherForecast> Get() { Console.WriteLine("服务响应"); //直接一,简单粗暴,不要拼写错了就好~~ Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] = "public, max-age=600"; var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
查看http响应头
上面的示例设置客户端缓存600秒,如果直接刷新浏览器或者按F5进行刷新,缓存会失效(cache-control对刷新无效)
2、用ResponseCacheAttribute类设置缓存
[HttpGet] [ResponseCache(Duration = 100)] public IEnumerable<WeatherForecast> Get() { Console.WriteLine("服务响应"); ////直接一,简单粗暴,不要拼写错了就好~~ //Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] = "public, max-age=600"; var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
效果和上面是一致的,通过源码分析发现ResponseCacheAttribute也是通过设置http头来实现的。
/// <summary> /// Specifies the parameters necessary for setting appropriate headers in response caching. /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class ResponseCacheAttribute : Attribute, IFilterFactory, IOrderedFilter { /// <inheritdoc /> public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) { if (serviceProvider == null) { throw new ArgumentNullException(nameof(serviceProvider)); } var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>(); var cacheProfile = GetCacheProfile(optionsAccessor.Value); // ResponseCacheFilter cannot take any null values. Hence, if there are any null values, // the properties convert them to their defaults and are passed on. return new ResponseCacheFilter(cacheProfile, loggerFactory); } }
ResponseCacheFilter部分代码如下
/// <summary> /// An <see cref="IActionFilter"/> which sets the appropriate headers related to response caching. /// </summary> internal class ResponseCacheFilter : IActionFilter, IResponseCacheFilter { /// <inheritdoc /> public void OnActionExecuting(ActionExecutingContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // If there are more filters which can override the values written by this filter, // then skip execution of this filter. var effectivePolicy = context.FindEffectivePolicy<IResponseCacheFilter>(); if (effectivePolicy != null && effectivePolicy != this) { _logger.NotMostEffectiveFilter(GetType(), effectivePolicy.GetType(), typeof(IResponseCacheFilter)); return; } _executor.Execute(context); } }
具体的实现是在ResponseCacheFilterExecutor类中,代码如下
internal class ResponseCacheFilterExecutor { public void Execute(FilterContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!NoStore) { // Duration MUST be set (either in the cache profile or in this filter) unless NoStore is true. if (_cacheProfile.Duration == null && _cacheDuration == null) { throw new InvalidOperationException( Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration))); } } 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 (VaryByQueryKeys != null) { var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>(); if (responseCachingFeature == null) { throw new InvalidOperationException( Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys))); } responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys; } 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 { string cacheControlValue; switch (Location) { case ResponseCacheLocation.Any: cacheControlValue = "public,"; break; case ResponseCacheLocation.Client: cacheControlValue = "private,"; break; case ResponseCacheLocation.None: cacheControlValue = "no-cache,"; headers[HeaderNames.Pragma] = "no-cache"; break; default: cacheControlValue = null; break; } cacheControlValue = $"{cacheControlValue}max-age={Duration}"; headers[HeaderNames.CacheControl] = cacheControlValue; } } }
通过源码分析已经知道了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,默认是缓存一个小时,还有一些其他可选配置也用注释的方式列了出来。
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(); }
当然,仅有这两句代码,并不能完成这里提到的服务端缓存。还需要前面客户端缓存的设置,两者结合起来才能起作用
- 第一次刷新的时候,会进入中间件,然后进入Action,返回结果,Fiddler记录到了这一次的请求
- 第二次打开新标签页,直接从浏览器缓存中返回的结果,即没有进入中间件,也没有进入Action,Fiddler也没有记录到相关请求
- 第三次换了一个浏览器,会进入中间件,直接由缓存返回结果,并没有进入Action,此时Fiddler也将该请求记录了下来,响应头包含了Age
注意:服务端缓存针对不同浏览器(不同客户端时)才能测试出效果(此时找不到浏览器缓存)
第三次请求响应头部的部分信息如下:
Age: 16
Cache-Control: public,max-age=600
这个Age是在变化的!它就等价于缓存的寿命
还有提到ResponseCacheAttribute中的VaryByQueryKeys这个属性,它需要结合ResponseCaching中间件一起用的
如果代码是这样写的
[HttpGet] [Route("[Action]")] [ResponseCache(Duration = 600)] public IActionResult List(int page = 0) { return Content(page.ToString()); }
静态文件缓存
对于静态文件,.NET Core有一个单独的StaticFiles中间件,如果想要对它做一些处理,同样需要在管道中进行注册。
UseStaticFiles
有几个重载方法,这里用的是带StaticFileOptions参数的那个方法。
因为StaticFileOptions里面有一个OnPrepareResponse可以让我们修改响应头,以达到HTTP缓存的效果。
下面来看个简单的例子:
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) }; } });