ASP.NET Core 中的响应缓存中间件

客户端(浏览器)缓存

通过设置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();
        }
View Code

查看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();
        }
View Code

 效果和上面是一致的,通过源码分析发现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);
        }
    }
View Code

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);
        }

         
    }
View Code

具体的实现是在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;
            }
        }
    }
View Code

通过源码分析已经知道了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" }
    });
});
View Code

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

[ResponseCache(CacheProfileName = "default")]
public IActionResult Index()
{
    return View();
}
View Code

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

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

服务端缓存

对比前面的客户端缓存,它是将东西存放在客户端,要用的时候就直接从客户端去取!

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

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

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

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCaching();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseResponseCaching();
}
View Code

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

  1. 第一次刷新的时候,会进入中间件,然后进入Action,返回结果,Fiddler记录到了这一次的请求
  2. 第二次打开新标签页,直接从浏览器缓存中返回的结果,即没有进入中间件,也没有进入Action,Fiddler也没有记录到相关请求
  3. 第三次换了一个浏览器,会进入中间件,直接由缓存返回结果,并没有进入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());
        }
View Code

静态文件缓存

对于静态文件,.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)
        };
    }
});
View Code

原文地址:https://www.cnblogs.com/Duko/p/14124189.html