简体   繁体   English

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

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

I have a API controller,and the scenario is: I need to consume third party datasource(let's say the third party is provided as a dll file for simplicity, and the dll contain Student model and StudentDataSource that contain a lot of method to retrieve student ), and calling the third party data source is costly and data only gets updated every 6 hours.我有一个 API controller,场景是:我需要使用第三方数据源(为了简单起见,假设第三方作为 dll 文件提供,dll 包含Student model 和StudentDataSource ,其中包含很多检索学生的方法),并且调用第三方数据源的成本很高,而且数据每 6 小时才更新一次。 so somehow I need to cache the output, below is some action method from my api controller:所以我需要以某种方式缓存 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));
}

then how should I cache the result especially for the second action method, it is dumb to cache every student result with different id那么我应该如何缓存结果,特别是对于第二个操作方法,缓存每个具有不同 id 的学生结果是愚蠢的

My team is implementing a similar caching strategy on an API controller using a custom Action filter attribute to handle the caching logic.我的团队正在 API 控制器上实施类似的缓存策略,使用自定义 Action 过滤器属性来处理缓存逻辑。 See here for more info on Action filters.有关操作过滤器的更多信息,请参见此处

The Action filter's OnActionExecuting method runs prior to your controller method, so you can check whether the data you're looking for is already cached and return it directly from here, bypassing the call to your third party datasource when cached data exists. Action 过滤器的OnActionExecuting方法在您的控制器方法之前运行,因此您可以检查您要查找的数据是否已经缓存并直接从这里返回,当缓存数据存在时绕过对第三方数据源的调用。 We also use this method to check the type of request and reset the cache on updates and deletes, but it sounds like you won't be modifying data.我们也使用这种方法来检查请求的类型并在更新和删除时重置缓存,但听起来您不会修改数据。

The Action filter's OnActionExecuted method runs immediately AFTER your controller method logic, giving you an opportunity to cache the response object before returning it to the client. Action 过滤器的OnActionExecuted方法在您的控制器方法逻辑之后立即运行,让您有机会在将响应对象返回给客户端之前缓存它。

The specifics of how you implement the actual caching are harder to provide an answer for, but Microsoft provides some options for in-memory caching in .NET Core (see MemoryCache.Default not available in .NET Core? )您如何实现实际缓存的细节很难提供答案,但 Microsoft 为 .NET Core 中的内存缓存提供了一些选项(请参阅MemoryCache.Default 在 .NET Core 中不可用?

I used the solution with the cache strategy through the controller API as @chris-brenberg pointed out, it turned out like this正如@chris-brenberg 指出的那样,我通过 controller API 使用了带有缓存策略的解决方案,结果是这样的

on controller class在 controller class

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

on ServerResponseCacheAttribute.cs在 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; }
    }
}}

I hope it helps someone, best regards我希望它能帮助别人,最好的问候

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

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