2021-03-22 12:09:25 -04:00
|
|
|
|
using System;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
using System.Collections.Concurrent;
|
2022-06-09 10:56:41 -04:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
2021-08-26 18:35:05 -04:00
|
|
|
|
using System.Threading;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Data.Abstractions;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
using Timer = System.Timers.Timer;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
|
|
|
|
|
namespace Data.Helpers
|
|
|
|
|
{
|
2021-11-15 11:25:55 -05:00
|
|
|
|
public class DataValueCache<TEntityType, TReturnType> : IDataValueCache<TEntityType, TReturnType>
|
|
|
|
|
where TEntityType : class
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
private readonly IDatabaseContextFactory _contextFactory;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
|
2022-06-09 10:56:41 -04:00
|
|
|
|
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
2023-04-19 20:55:33 -04:00
|
|
|
|
private readonly string _defaultKey = null;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
|
|
|
|
|
private bool _autoRefresh;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
private const int DefaultExpireMinutes = 15;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
private Timer _timer;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
|
2021-11-21 16:33:44 -05:00
|
|
|
|
private class CacheState<TCacheType>
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
|
|
|
|
public string Key { get; set; }
|
|
|
|
|
public DateTime LastRetrieval { get; set; }
|
|
|
|
|
public TimeSpan ExpirationTime { get; set; }
|
2023-04-19 20:55:33 -04:00
|
|
|
|
public Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TCacheType>> Getter { get; set; }
|
2021-11-21 16:33:44 -05:00
|
|
|
|
public TCacheType Value { get; set; }
|
|
|
|
|
public bool IsSet { get; set; }
|
2021-08-29 14:10:10 -04:00
|
|
|
|
|
|
|
|
|
public bool IsExpired => ExpirationTime != TimeSpan.MaxValue &&
|
|
|
|
|
(DateTime.Now - LastRetrieval.Add(ExpirationTime)).TotalSeconds > 0;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
}
|
2021-08-29 14:10:10 -04:00
|
|
|
|
|
2021-11-15 11:25:55 -05:00
|
|
|
|
public DataValueCache(ILogger<DataValueCache<TEntityType, TReturnType>> logger,
|
|
|
|
|
IDatabaseContextFactory contextFactory)
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
|
|
|
|
_logger = logger;
|
|
|
|
|
_contextFactory = contextFactory;
|
|
|
|
|
}
|
2021-08-29 14:10:10 -04:00
|
|
|
|
|
2021-11-15 11:25:55 -05:00
|
|
|
|
~DataValueCache()
|
|
|
|
|
{
|
|
|
|
|
_timer?.Stop();
|
|
|
|
|
_timer?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
|
|
|
|
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
2023-04-19 20:55:33 -04:00
|
|
|
|
SetCacheItem((set, _, token) => getter(set, token), key, null, expirationTime, autoRefresh);
|
2022-06-09 10:56:41 -04:00
|
|
|
|
}
|
2021-08-29 14:10:10 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
public void SetCacheItem(Func<DbSet<TEntityType>, IEnumerable<object>, CancellationToken, Task<TReturnType>> getter, string key,
|
2022-06-09 10:56:41 -04:00
|
|
|
|
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
|
|
|
|
{
|
2022-06-12 13:19:32 -04:00
|
|
|
|
ids ??= new[] { _defaultKey };
|
2023-04-19 20:55:33 -04:00
|
|
|
|
|
2022-06-09 10:56:41 -04:00
|
|
|
|
if (!_cacheStates.ContainsKey(key))
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
2022-06-09 10:56:41 -04:00
|
|
|
|
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
|
|
|
|
}
|
2021-11-15 11:25:55 -05:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
var cacheInstance = _cacheStates[key];
|
|
|
|
|
var id = GenerateKeyFromIds(ids);
|
2023-04-04 22:53:01 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
lock (_cacheStates)
|
|
|
|
|
{
|
|
|
|
|
if (cacheInstance.ContainsKey(id))
|
2022-06-09 10:56:41 -04:00
|
|
|
|
{
|
2023-04-19 20:55:33 -04:00
|
|
|
|
return;
|
2022-06-09 10:56:41 -04:00
|
|
|
|
}
|
2023-04-19 20:55:33 -04:00
|
|
|
|
}
|
2022-06-09 10:56:41 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
var state = new CacheState<TReturnType>
|
|
|
|
|
{
|
|
|
|
|
Key = key,
|
|
|
|
|
Getter = getter,
|
|
|
|
|
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
|
|
|
|
};
|
2022-06-09 10:56:41 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
lock (_cacheStates)
|
|
|
|
|
{
|
|
|
|
|
cacheInstance.Add(id, state);
|
|
|
|
|
}
|
2022-06-09 10:56:41 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
_autoRefresh = autoRefresh;
|
2022-06-09 10:56:41 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
|
|
|
|
{
|
|
|
|
|
return;
|
2021-11-15 11:25:55 -05:00
|
|
|
|
}
|
2023-04-19 20:55:33 -04:00
|
|
|
|
|
|
|
|
|
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
|
|
|
|
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, ids, CancellationToken.None);
|
|
|
|
|
_timer.Start();
|
2021-03-22 12:09:25 -04:00
|
|
|
|
}
|
2021-08-29 14:10:10 -04:00
|
|
|
|
|
2023-04-04 22:53:01 -04:00
|
|
|
|
public Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
|
|
|
|
GetCacheItem(keyName, null, cancellationToken);
|
2022-06-09 10:56:41 -04:00
|
|
|
|
|
2023-04-19 20:55:33 -04:00
|
|
|
|
public async Task<TReturnType> GetCacheItem(string keyName, IEnumerable<object> ids = null,
|
2022-06-09 10:56:41 -04:00
|
|
|
|
CancellationToken cancellationToken = default)
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
|
|
|
|
if (!_cacheStates.ContainsKey(keyName))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("No cache found for key {key}", keyName);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 22:53:01 -04:00
|
|
|
|
var cacheInstance = _cacheStates[keyName];
|
|
|
|
|
|
|
|
|
|
CacheState<TReturnType> state;
|
|
|
|
|
|
|
|
|
|
lock (_cacheStates)
|
|
|
|
|
{
|
2023-04-19 20:55:33 -04:00
|
|
|
|
state = ids is null ? cacheInstance.Values.First() : _cacheStates[keyName][GenerateKeyFromIds(ids)];
|
2023-04-04 22:53:01 -04:00
|
|
|
|
}
|
2021-03-22 12:09:25 -04:00
|
|
|
|
|
2021-11-15 11:25:55 -05:00
|
|
|
|
// when auto refresh is off we want to check the expiration and value
|
|
|
|
|
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
|
2021-11-21 16:33:44 -05:00
|
|
|
|
if ((state.IsExpired || !state.IsSet) && !_autoRefresh || _autoRefresh && !state.IsSet)
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
2023-04-19 20:55:33 -04:00
|
|
|
|
await RunCacheUpdate(state, ids, cancellationToken);
|
2021-03-22 12:09:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.Value;
|
|
|
|
|
}
|
2023-04-19 20:55:33 -04:00
|
|
|
|
|
|
|
|
|
private async Task RunCacheUpdate(CacheState<TReturnType> state, IEnumerable<object> ids, CancellationToken token)
|
2021-03-22 12:09:25 -04:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2021-11-15 11:25:55 -05:00
|
|
|
|
_logger.LogDebug("Running update for {ClassName} {@State}", GetType().Name, state);
|
2021-03-22 12:09:25 -04:00
|
|
|
|
await using var context = _contextFactory.CreateContext(false);
|
2021-11-15 11:25:55 -05:00
|
|
|
|
var set = context.Set<TEntityType>();
|
2023-04-19 20:55:33 -04:00
|
|
|
|
var value = await state.Getter(set, ids, token);
|
2021-03-22 12:09:25 -04:00
|
|
|
|
state.Value = value;
|
2021-11-21 16:33:44 -05:00
|
|
|
|
state.IsSet = true;
|
2021-03-22 12:09:25 -04:00
|
|
|
|
state.LastRetrieval = DateTime.Now;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2021-11-15 11:25:55 -05:00
|
|
|
|
_logger.LogError(ex, "Could not get cached value for {Key}", state.Key);
|
2021-03-22 12:09:25 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-19 20:55:33 -04:00
|
|
|
|
|
|
|
|
|
private static string GenerateKeyFromIds(IEnumerable<object> ids) =>
|
|
|
|
|
string.Join("_", ids.Select(id => id?.ToString() ?? "null"));
|
2021-03-22 12:09:25 -04:00
|
|
|
|
}
|
2022-06-09 10:56:41 -04:00
|
|
|
|
}
|