using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.Cache { #nullable enable public static class EfCacheL2 { public static int RequestsToDb = 0; static readonly ConcurrentDictionary> caches = new(1, 16); private const int semaphoreTimeout = 25_000; private static readonly SemaphoreSlim semaphore = new(1); private struct CacheItem { public readonly IEnumerable Data; public readonly DateTime DateObsolete; public CacheItem(IEnumerable data, TimeSpan obsolescence) { DateObsolete = DateTime.Now + obsolescence; Data = data; } } private static CacheItem GetOrAddCache(string tag, Func valueFactory) { Lazy? lazyCache; while (!caches.TryGetValue(tag, out lazyCache)) { if (semaphore.Wait(0)) { lazyCache = new Lazy(valueFactory); caches.TryAdd(tag, lazyCache); _ = lazyCache.Value; semaphore.Release(); break; } else { if(semaphore.Wait(semaphoreTimeout)) semaphore.Release(); else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); } } } if (lazyCache.Value.DateObsolete < DateTime.Now) { var isUpdated = false; if (semaphore.Wait(0)) { lazyCache = new Lazy(valueFactory); caches.Remove(tag, out _); caches.TryAdd(tag, lazyCache); _ = lazyCache.Value; isUpdated = true; semaphore.Release(); } else { if (semaphore.Wait(semaphoreTimeout)) semaphore.Release(); else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); } } if (isUpdated || caches.TryGetValue(tag, out lazyCache)) return lazyCache.Value; throw new Exception("EfCacheL2.GetOrAddCache it should never happens"); } else return lazyCache.Value; } private static async Task GetOrAddCacheAsync(string tag, Func valueFactory, CancellationToken token) { Lazy? lazyCache; while (!caches.TryGetValue(tag, out lazyCache)) { if (semaphore.Wait(0, CancellationToken.None)) { lazyCache = new Lazy(valueFactory); caches.TryAdd(tag, lazyCache); _ = lazyCache.Value; semaphore.Release(); break; } else { if (await semaphore.WaitAsync(semaphoreTimeout, token)) semaphore.Release(); else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); } } } if (lazyCache.Value.DateObsolete < DateTime.Now) { var isUpdated = false; if (semaphore.Wait(0, CancellationToken.None)) { lazyCache = new Lazy(valueFactory); caches.Remove(tag, out _); caches.TryAdd(tag, lazyCache); _ = lazyCache.Value; isUpdated = true; semaphore.Release(); } else { if (await semaphore.WaitAsync(semaphoreTimeout, token)) semaphore.Release(); else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); } } if (isUpdated || caches.TryGetValue(tag, out lazyCache)) return lazyCache.Value; throw new Exception("EfCacheL2.GetOrAddCache it should never happens"); } else return lazyCache.Value; } private static IEnumerable ConvertToIEnumerable(IEnumerable data) { if (data is IEnumerable list) return list; else if (data is IDictionary dictionary) { System.Diagnostics.Trace.TraceWarning($"ConvertToIEnumerable. Use keyless method on keyed cache. Type: {typeof(T).Name};"); return (IEnumerable)dictionary.Values; } else throw new NotSupportedException("cache.Data has wrong type."); } private static Dictionary ConvertToDictionary(IEnumerable data, Func keySelector) where TKey : notnull { if (data is Dictionary dictionary) return dictionary; else { System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};"); return ((IEnumerable)data).ToDictionary(keySelector); } } private static Func MakeValueListFactory(IQueryable query, TimeSpan obsolescence) { CacheItem ValueFactory() { var list = query.ToList(); RequestsToDb++; return new CacheItem(list, obsolescence); } return ValueFactory; } private static Func MakeValueDictionaryFactory(IQueryable query, TimeSpan obsolescence, Func keySelector) where TKey : notnull { CacheItem ValueFactory() { var dictionary = query.ToDictionary(keySelector); RequestsToDb++; return new CacheItem(dictionary, obsolescence); }; return ValueFactory; } public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence) { var factory = MakeValueListFactory(query, obsolescence); var cache = GetOrAddCache(tag, factory); return ConvertToIEnumerable(cache.Data); } public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) { var factory = MakeValueListFactory(query, obsolescence); var cache = await GetOrAddCacheAsync(tag, factory, token); return ConvertToIEnumerable(cache.Data); } public static Dictionary FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector) where TKey: notnull { var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); var cache = GetOrAddCache(tag, factory); return ConvertToDictionary(cache.Data, keySelector); } public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, CancellationToken token = default) where TKey : notnull { var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); var cache = await GetOrAddCacheAsync(tag, factory, token); return ConvertToDictionary(cache.Data, keySelector); } public static T? FromCacheGetValueOrDefault(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key) where TKey : notnull { var factory = MakeValueDictionaryFactory(query, obsolescence, keySelector); var cache = GetOrAddCache(tag, factory); if (cache.Data is Dictionary dictionary) return dictionary.GetValueOrDefault(key); else { System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};"); return ((IEnumerable)cache.Data).FirstOrDefault(v => keySelector(v).Equals(key)); } } public static void DropCache(this IQueryable query, string tag) { caches.Remove(tag, out var _); } } #nullable disable }