简体   繁体   中英

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. so somehow I need to cache the output, below is some action method from my 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

My team is implementing a similar caching strategy on an API controller using a custom Action filter attribute to handle the caching logic. 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. 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.

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? )

I used the solution with the cache strategy through the controller API as @chris-brenberg pointed out, it turned out like this

on 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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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