using Microsoft.EntityFrameworkCore; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace AsbCloudInfrastructure.EfCache { #nullable enable /// <summary> /// Кеширование запросов EF.<br/> /// Кеш не отслеживается ChangeTracker. /// </summary> public static class EfCacheExtensions { private static readonly Dictionary<string, CacheItem> 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); internal IEnumerable<TEntity> GetData<TEntity>() { if (Data is IEnumerable<TEntity> typedData) return typedData; throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); } internal IEnumerable<TModel> GetData<TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1) { if (Data is IEnumerable<TModel> typedData) return typedData; if (Data is IEnumerable<TEntity> typedEntityData) { if (semaphore.Wait(0)) { try { var convertedData = typedEntityData.Select(convert).ToList(); Data = convertedData; return convertedData; } catch { throw; } finally { semaphore.Release(); } } else { if (semaphore.Wait(semaphoreTimeout)) { semaphore.Release(); } else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while converting cache data"); } } } if(attempt > 0) return GetData(convert, --attempt); throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); } } private static CacheItem GetOrAddCache(string tag, Func<IEnumerable> 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<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<IEnumerable>> 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; } /// <summary> /// Кешировать запрос в List<<typeparamref name="TEntity"/>>. /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="query"></param> /// <param name="tag">Метка кеша</param> /// <param name="obsolescence">Период устаревания данных</param> /// <returns></returns> public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence) where TEntity : class { IEnumerable factory() => query.AsNoTracking().ToList(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache.GetData<TEntity>(); } /// <summary> /// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/> /// Преобразование выполняется после получения из БД, результат кешируется в List<<typeparamref name="TEntity"/>>. /// </summary> /// <typeparam name="TEntity"></typeparam> /// <typeparam name="TModel"></typeparam> /// <param name="query"></param> /// <param name="tag">Метка кеша</param> /// <param name="obsolescence">Период устаревания данных</param> /// <param name="convert">Преобразование данных БД в DTO</param> /// <returns></returns> public static IEnumerable<TModel> FromCache<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert) where TEntity : class { IEnumerable factory () => query.AsNoTracking().ToList(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache.GetData(convert); } /// <summary> /// Асинхронно кешировать запрос в List<<typeparamref name="TEntity"/>>.<br/> /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="query"></param> /// <param name="tag">Метка кеша</param> /// <param name="obsolescence">Период устаревания данных</param> /// <param name="token"></param> /// <returns></returns> public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token = default) where TEntity : class { async Task<IEnumerable> factory(CancellationToken token) => await query.AsNoTracking().ToListAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache.GetData<TEntity>(); } /// <summary> /// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/> /// Преобразование выполняется после получения из БД, результат кешируется в List<<typeparamref name="TModel"/>>. /// </summary> /// <typeparam name="TEntity"></typeparam> /// <typeparam name="TModel"></typeparam> /// <param name="query"></param> /// <param name="tag">Метка кеша</param> /// <param name="obsolescence">Период устаревания данных</param> /// <param name="convert">Преобразование данных БД в DTO</param> /// <param name="token"></param> /// <returns></returns> public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token = default) where TEntity : class { async Task<IEnumerable> factory(CancellationToken token) => await query.AsNoTracking().ToListAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache.GetData(convert); } /// <summary> /// Очистить кеш /// </summary> /// <typeparam name="T"></typeparam> /// <param name="query"></param> /// <param name="tag">Метка кеша</param> public static void DropCache<T>(this IQueryable<T> query, string tag) { caches.Remove(tag, out var _); } } #nullable disable }