using Microsoft.EntityFrameworkCore; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.Services.Cache { #nullable enable /// /// Кеширование запросов EF. /// public static class EfCacheL2 { /// /// Кол-во обращений к БД. /// public static int CountOfRequestsToDB = 0; private static readonly Dictionary caches = new(16); private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25); private static readonly SemaphoreSlim semaphore = new(1); private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2); private class CacheItem { internal IEnumerable? Data; internal DateTime DateObsolete; internal DateTime DateObsoleteTotal; internal readonly SemaphoreSlim semaphore = new(1); } private static CacheItem GetOrAddCache(string tag, Func valueFactory, TimeSpan obsolete) { CacheItem cache; while (!caches.ContainsKey(tag)) { if (semaphore.Wait(0)) { try { cache = new CacheItem(); var dateObsolete = DateTime.Now + obsolete; var dateQueryStart = DateTime.Now; var data = valueFactory(); var queryTime = DateTime.Now - dateQueryStart; if (dateObsolete - DateTime.Now < minCacheTime) dateObsolete = DateTime.Now + minCacheTime; cache.Data = data; cache.DateObsolete = dateObsolete; cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime; caches.Add(tag, cache); } catch { throw; } finally { 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"); } } } cache = caches[tag]; if (cache.DateObsolete < DateTime.Now) { if (cache.semaphore.Wait(0)) { try { var dateObsolete = DateTime.Now + obsolete; var dateQueryStart = DateTime.Now; var data = valueFactory(); var queryTime = DateTime.Now - dateQueryStart; if (dateObsolete - DateTime.Now < minCacheTime) dateObsolete = DateTime.Now + minCacheTime; cache.Data = data; cache.DateObsolete = dateObsolete; cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime; } catch { throw; } finally { cache.semaphore.Release(); } } else if(cache.DateObsoleteTotal < DateTime.Now) { if (cache.semaphore.Wait(semaphoreTimeout)) { cache.semaphore.Release(); } else { cache.semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache"); } } } return cache; } private static async Task GetOrAddCacheAsync(string tag, Func> valueFactoryAsync, TimeSpan obsolete, CancellationToken token) { CacheItem cache; while (!caches.ContainsKey(tag)) { if (semaphore.Wait(0)) { try { cache = new CacheItem(); var dateObsolete = DateTime.Now + obsolete; var dateQueryStart = DateTime.Now; var data = await valueFactoryAsync(token); var queryTime = DateTime.Now - dateQueryStart; if (dateObsolete - DateTime.Now < minCacheTime) dateObsolete = DateTime.Now + minCacheTime; cache.Data = data; cache.DateObsolete = dateObsolete; cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime; caches.Add(tag, cache); } catch { throw; } finally { 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"); } } } cache = caches[tag]; if (cache.DateObsolete < DateTime.Now) { if (cache.semaphore.Wait(0)) { try { var dateObsolete = DateTime.Now + obsolete; var dateQueryStart = DateTime.Now; var data = await valueFactoryAsync(token); var queryTime = DateTime.Now - dateQueryStart; if (dateObsolete - DateTime.Now < minCacheTime) dateObsolete = DateTime.Now + minCacheTime; cache.Data = data; cache.DateObsolete = dateObsolete; cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime; } catch { throw; } finally { cache.semaphore.Release(); } } else if (cache.DateObsoleteTotal < DateTime.Now) { if (await cache.semaphore.WaitAsync(semaphoreTimeout, token)) { cache.semaphore.Release(); } else { cache.semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache"); } } } return cache; } 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 if (data is IEnumerable enumerable) { System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};"); return enumerable.ToDictionary(keySelector); } else throw new NotSupportedException("cache.Data has wrong type."); } /// /// Кешировать запрос в List\. /// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря. /// /// /// /// Метка кеша /// Период устаревания данных /// public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence) { IEnumerable factory () { CountOfRequestsToDB++; return query.ToList(); } var cache = GetOrAddCache(tag, factory, obsolescence); return ConvertToIEnumerable(cache.Data); } /// /// Асинхронно кешировать запрос в List\. /// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря. /// /// /// /// Метка кеша /// Период устаревания данных /// /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) { async Task factory(CancellationToken token) { CountOfRequestsToDB++; return await query.ToListAsync(token); } var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return ConvertToIEnumerable(cache.Data); } /// /// Кешировать запрос в Dictionary\. /// /// тип ключа /// тип значения /// /// Метка кеша /// Период устаревания данных /// Делегат получения ключа из записи /// /// /// /// public static Dictionary FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector) where TKey: notnull { IEnumerable factory () { CountOfRequestsToDB++; return query.ToDictionary(keySelector); } var cache = GetOrAddCache(tag, factory, obsolescence); return ConvertToDictionary(cache.Data, keySelector); } /// /// Асинхронно кешировать запрос в Dictionary\. /// /// тип ключа /// тип значения /// /// Метка кеша /// Период устаревания данных /// Делегат получения ключа из записи /// /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, CancellationToken token = default) where TKey : notnull { async Task factory(CancellationToken token) { CountOfRequestsToDB++; return await query.ToDictionaryAsync(keySelector, token); } var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return ConvertToDictionary(cache.Data, keySelector); } /// /// Получить запись из кеша по ключу. /// При отсутствии кеша создаст его для всех записей из query. /// /// тип ключа /// тип значения /// /// Метка кеша /// Период устаревания данных /// Делегат получения ключа из записи /// /// /// if cache contains trash public static T? FromCacheGetValueOrDefault(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key) where TKey : notnull { IEnumerable factory() { CountOfRequestsToDB++; return query.ToDictionary(keySelector); } var cache = GetOrAddCache(tag, factory, obsolescence); var data = cache.Data; if (data is Dictionary dictionary) return dictionary.GetValueOrDefault(key); else if (data is IEnumerable enumerable) { System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};"); return enumerable.FirstOrDefault(v => keySelector(v).Equals(key)); } else throw new NotSupportedException("cache.Data has wrong type."); } /// /// Асинхронно получить запись из кеша по ключу. /// При отсутствии кеша создаст его для всех записей из query. /// /// тип ключа /// тип значения /// /// Метка кеша /// Период устаревания данных /// Делегат получения ключа из записи /// /// /// /// if cache contains trash public static async Task FromCacheGetValueOrDefaultAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key, CancellationToken token = default) where TKey : notnull { async Task factory(CancellationToken token) { CountOfRequestsToDB++; return await query.ToDictionaryAsync(keySelector, token); } var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); var data = cache.Data; if (data is Dictionary dictionary) return dictionary.GetValueOrDefault(key); else if (data is IEnumerable enumerable) { System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};"); return enumerable.FirstOrDefault(v => keySelector(v).Equals(key)); } else throw new NotSupportedException("cache.Data has wrong type."); } public static void DropCache(this IQueryable query, string tag) { caches.Remove(tag, out var _); } } #nullable disable }