簡體   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