繁体   English   中英

如何在 ASP.NET 核心中缓存 output

[英]how do I cache an output in ASP.NET Core

我有一个 API controller,场景是:我需要使用第三方数据源(为了简单起见,假设第三方作为 dll 文件提供,dll 包含Student model 和StudentDataSource ,其中包含很多检索学生的方法),并且调用第三方数据源的成本很高,而且数据每 6 小时才更新一次。 所以我需要以某种方式缓存 output,下面是我的 api controller 中的一些操作方法:

// api controller that contain action methods below

[HttpGet]
public JsonResult GetAllStudentRecords()
{
   var dataSource = new StudentDataSource();  
   return Json(dataSource.GetAllStudents());
}

[HttpGet("{id}")]
public JsonResult GetStudent(int id)
{
   var dataSource = new StudentDataSource();
   return Json(dataSource.getStudent(id));
}

那么我应该如何缓存结果,特别是对于第二个操作方法,缓存每个具有不同 id 的学生结果是愚蠢的

我的团队正在 API 控制器上实施类似的缓存策略,使用自定义 Action 过滤器属性来处理缓存逻辑。 有关操作过滤器的更多信息,请参见此处

Action 过滤器的OnActionExecuting方法在您的控制器方法之前运行,因此您可以检查您要查找的数据是否已经缓存并直接从这里返回,当缓存数据存在时绕过对第三方数据源的调用。 我们也使用这种方法来检查请求的类型并在更新和删除时重置缓存,但听起来您不会修改数据。

Action 过滤器的OnActionExecuted方法在您的控制器方法逻辑之后立即运行,让您有机会在将响应对象返回给客户端之前缓存它。

您如何实现实际缓存的细节很难提供答案,但 Microsoft 为 .NET Core 中的内存缓存提供了一些选项(请参阅MemoryCache.Default 在 .NET Core 中不可用?

正如@chris-brenberg 指出的那样,我通过 controller API 使用了带有缓存策略的解决方案,结果是这样的

在 controller class

[ServerResponseCache(false)]
[HttpGet]
[Route("cache")]
public ActionResult GetCache(string? dateFormat) {
    Logger.LogInformation("Getting current datetime");
    return Ok(new { date = DateTime.Now.ToString() });
}

在 ServerResponseCacheAttribute.cs 上

namespace Site.Api.Filters {
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System.Globalization;
using System.Threading.Tasks;

public class ServerResponseCacheAttribute : TypeFilterAttribute {

    public ServerResponseCacheAttribute(bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
        Arguments = new object[] { new ServerResponseCacheProps { ByUserContext = byUserContext } };

    public ServerResponseCacheAttribute(int secondsTimeout, bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
        Arguments = new object[] { new ServerResponseCacheProps { SecondsTimeout = secondsTimeout, ByUserContext = byUserContext } };

    public class ServerResponseCacheProps {

        public int? SecondsTimeout { get; set; }

        public bool ByUserContext { get; set; }
    }

    public class ServerResponseCacheConfig {

        public bool Disabled { get; set; }

        public int SecondsTimeout { get; set; } = 60;

        public string[] HeadersOnCache { get; set; } = { "Accept-Language" };
    }

    private class ServerResponseCacheAttributeImplementation : IAsyncActionFilter {

        private string _cacheKey = default;

        readonly ILogger<ServerResponseCacheAttributeImplementation> _logger;

        readonly IMemoryCache _memoryCache;

        readonly ServerResponseCacheConfig _config;

        readonly bool _byUserContext;

        public ServerResponseCacheAttributeImplementation(ILogger<ServerResponseCacheAttributeImplementation> logger,
            IMemoryCache memoryCache, ServerResponseCacheProps props) {
            _logger = logger;
            _memoryCache = memoryCache;
            _byUserContext = props.ByUserContext;
            _config = new ServerResponseCacheConfig {
                SecondsTimeout = props.SecondsTimeout ?? 60,
                HeadersOnCache = new[] { "Accept-Language" }
            };
        }

        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
            if (context == null) {
                throw new ArgumentNullException(nameof(context));
            }
            if (next == null) {
                throw new ArgumentNullException(nameof(next));
            }

            if (_config.Disabled) {
                await next();
                return;
            }

            OnActionExecutingAsync(context);

            if (context.Result == null) {
                OnActionExecuted(await next());
            }
        }

        void OnActionExecutingAsync(ActionExecutingContext context) {
            SetCacheKey(context.HttpContext.Request);

            // Not use a stored response to satisfy the request. Will regenerates the response for the client, and updates the stored response in its cache.
            bool noCache = context.HttpContext.Request.Headers.CacheControl.Contains("no-cache");
            if (noCache) {
                return;
            }

            TryLoadResultFromCache(context);
        }

        void SetCacheKey(HttpRequest request) {
            if (request == null) {
                throw new ArgumentException(nameof(request));
            }

            if (!string.Equals(request.Method, "GET", StringComparison.InvariantCultureIgnoreCase)) {
                return;
            }

            List<string> cacheKeys = new List<string>();

            if (_byUserContext && request.HttpContext.User.Identity.IsAuthenticated) {
                cacheKeys.Add($"{request.HttpContext.User.Identity.Name}");
            }

            string uri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString);
            cacheKeys.Add(uri);

            foreach (string headerKey in _config.HeadersOnCache) {
                StringValues headerValue;
                if (request.Headers.TryGetValue(headerKey, out headerValue)) {
                    cacheKeys.Add($"{headerKey}:{headerValue}");
                }
            }

            _cacheKey = string.Join('_', cacheKeys).ToLower();
        }

        void TryLoadResultFromCache(ActionExecutingContext context) {
            ResultCache resultCache;
            if (_cacheKey != null && _memoryCache.TryGetValue(_cacheKey, out resultCache)) {
                _logger.LogInformation("ServerResponseCache: Response loaded from cache, cacheKey: {cacheKey}, expires at: {expiration}.", _cacheKey, resultCache.Expiration);

                context.Result = resultCache.Result;
                SetExpiresHeader(context.HttpContext.Response, resultCache.Expiration);
            }
        }

        /// <summary>Add expires header (the time after which the response is considered stale).</summary>
        void SetExpiresHeader(HttpResponse response, DateTimeOffset expiration) {
            string expireHttpDate = expiration.UtcDateTime.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
            response.Headers.Add("Expires", $"{expireHttpDate} GMT");
        }

        void OnActionExecuted(ActionExecutedContext context) {
            if (_cacheKey == null) {
                return;
            }

            if (context.Result != null) {
                DateTimeOffset expiration = SetCache(context.Result);
                SetExpiresHeader(context.HttpContext.Response, expiration);

            } else {
                RemoveCache();
            }
        }

        DateTimeOffset SetCache(IActionResult result) {
            DateTimeOffset absoluteExpiration = DateTimeOffset.Now.AddSeconds(_config.SecondsTimeout);

            ResultCache resultCache = new ResultCache {
                Result = result,
                Expiration = absoluteExpiration
            };
            _memoryCache.Set(_cacheKey, resultCache, absoluteExpiration);

            _logger.LogInformation("ServerResponseCache: Response set on cache, cacheKey: {cacheKey}, until: {expiration}.", _cacheKey, absoluteExpiration);

            return absoluteExpiration;
        }

        void RemoveCache() {
            _memoryCache.Remove(_cacheKey);

            _logger.LogInformation("ServerResponseCache: Response removed from cache, cacheKey: {cacheKey}.", _cacheKey);
        }
    }

    private class ResultCache {

        public IActionResult Result { get; set; }

        public DateTimeOffset Expiration { get; set; }
    }
}}

我希望它能帮助别人,最好的问候

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM