Я нашел похожий вопрос Кэширование в памяти с авторегенерацией на ASP.Net Core , в котором предлагалось вызвать
AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_options.ReferenceDataRefreshTimeSpan).Token)
.
Я попробовал, но не помог. Однако тот же ответ имел альтернативный (и рекомендуемый) вариант с использованием таймера. Я создал класс RefreshebleCache, который я использую для различных кэшируемых опций, таких как:
var refreshebleCache = new RefreshebleCache<MyCashableObjectType>(_cache, _logger);
Task<MyCashableObjectType> CacheableAsyncFunc() => GetMyCashableObjectTypeFromApiAsync();
var cachedResponse = await refreshebleCache.GetOrAddAsync("MyCashableObject", CacheableAsyncFunc,
_options.RefreshTimeSpan);
Реализация RefreshebleCache:
/// <summary>
/// Based on https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core
/// </summary>
/// <typeparam name="T"></typeparam>
public class RefreshebleCache<T>
{
protected readonly IAppCache _cache;
private readonly ILogger _logger;
public bool LoadingBusy = false;
private string _cacheKey;
private TimeSpan _refreshTimeSpan;
private Func<Task<T>> _functionToLoad;
private Timer _timer;
public RefreshebleCache(IAppCache cache, ILogger logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T> GetOrAddAsync (string cacheKey , Func<Task<T>> functionToLoad, TimeSpan refreshTimeSpan)
{
_refreshTimeSpan= refreshTimeSpan;
_functionToLoad = functionToLoad;
_cacheKey = cacheKey;
_timer = _cache.GetOrAdd("Timer_for_"+cacheKey, () => CreateTimer(refreshTimeSpan));
var cachedValue = await LoadCacheEntryAsync();
return cachedValue;
}
private Timer CreateTimer(TimeSpan refreshTimeSpan)
{
Debug.WriteLine($"calling CreateTimer for {_cacheKey} refreshTimeSpan {refreshTimeSpan}"); //start first time in refreshTimeSpan
return new Timer(TimerTickAsync, null, refreshTimeSpan, refreshTimeSpan);
}
private async void TimerTickAsync(object state)
{
if (LoadingBusy) return;
try
{
LoadingBusy = true;
Debug.WriteLine($"calling LoadCacheEntryAsync from TimerTickAsync for {_cacheKey}");
var loadingTask = LoadCacheEntryAsync(true);
await loadingTask;
}
catch(Exception e)
{
_logger.LogWarning($" {nameof(T)} for {_cacheKey} was not reloaded. {e} ");
}
finally
{
LoadingBusy = false;
}
}
private async Task<T> LoadCacheEntryAsync(bool update=false)
{
var cacheEntryOptions = SetMemoryCacheEntryOptions();
Func<Task<T>> cacheableAsyncFunc = () => _functionToLoad();
Debug.WriteLine($"called LoadCacheEntryAsync for {_cacheKey} update:{update}");
T cachedValues = default(T);
if (update)
{
cachedValues =await cacheableAsyncFunc();
if (cachedValues != null)
{
_cache.Add(_cacheKey, cachedValues, cacheEntryOptions);
}
// _cache.Add(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
}
else
{
cachedValues = await _cache.GetOrAddAsync(_cacheKey, cacheableAsyncFunc, cacheEntryOptions);
}
return cachedValues;
}
private MemoryCacheEntryOptions SetMemoryCacheEntryOptions()
{
//Make Expiration a bit bigger( =1.1) than _refreshTimeSpan, to let refresh before expiry
TimeSpan expirationTimeSpan = new TimeSpan(Convert.ToInt64(_refreshTimeSpan.Ticks * 1.1));
var cacheEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expirationTimeSpan
};
//I've attempted to use AddExpirationToken and RegisterPostEvictionCallback, but wasn't able to invoke reliably.
// Instead timer from https://stackoverflow.com/questions/44723017/in-memory-caching-with-auto-regeneration-on-asp-net-core seems working
//.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(_refreshTimeSpan).Token))
//.RegisterPostEvictionCallback(
// async (key, value, reason, state) =>
// {
// await GetOrAddAsync(_cacheKey, _functionToLoad, _refreshTimeSpan);//suppose to save to cache
// _logger.LogInformation($" {nameof(T)} are reloaded ");
// });
return cacheEntryOptions;
}