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 /// /// Кеширование запросов EF.
/// Кеш не отслеживается ChangeTracker. ///
public static class EfCacheExtensions { 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 static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4); private class YieldConvertedData : IEnumerable { private struct ConvertedData { public TEntity? Entity; public TModel? Model; } ConvertedData[] data; public Func convert { get; } public YieldConvertedData(TEntity[] entities, Func convert) { data = (entities.Select(x => new ConvertedData { Entity = x, Model = default })) .ToArray(); this.convert = convert; } class YieldConvertedDataEnumerator : IEnumerator { private readonly ConvertedData[] data; private readonly Func convert; private int position = -1; public YieldConvertedDataEnumerator(ConvertedData[] data, Func convert) { this.data = data; this.convert = convert; } public TModel Current { get { if (data[position].Entity is TEntity entity) { var dto = convert(entity); data[position].Entity = default; data[position].Model = dto; } return data[position].Model!; } } object IEnumerator.Current => Current!; public void Dispose() { } public bool MoveNext() { position++; return (position < data.Length); } public void Reset() { position = -1; } } public IEnumerator GetEnumerator() { var result = new YieldConvertedDataEnumerator(data, convert); return result; } IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } private class CacheItem { internal IEnumerable? Data; internal DateTime DateObsolete; internal DateTime DateObsoleteTotal; internal readonly SemaphoreSlim semaphore = new(1); internal IEnumerable GetData() { if (Data is IEnumerable typedData) return typedData; throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique."); } internal IEnumerable GetData(Func convert, int attempt = 1) { if (Data is IEnumerable typedData) return typedData; if (Data is IEnumerable typedEntityData) { if (semaphore.Wait(0)) { try { var convertedData = new YieldConvertedData(typedEntityData.ToArray(), convert); Data = convertedData; return convertedData; } finally { semaphore.Release(); } } else { if (semaphore.Wait(semaphoreTimeout)) { semaphore.Release(); } else { semaphore.Release(); throw new TimeoutException("EfCacheL2.GetData. 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 valueFactory, TimeSpan obsolete) { CacheItem cache; while (!caches.ContainsKey(tag)) { if (semaphore.Wait(0)) { try { if (!caches.ContainsKey(tag)) { cache = new CacheItem(); caches.Add(tag, cache); } } 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; } 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 { if (!caches.ContainsKey(tag)) { cache = new CacheItem(); caches.Add(tag, cache); } } 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; } 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; } /// /// Кешировать запрос в List<>. Кеш tag = typeof(TEntity).Name /// /// /// /// public static IEnumerable FromCache(this IQueryable query) where TEntity : class { var tag = typeof(TEntity).Name; return FromCache(query, tag, defaultObsolescence); } /// /// Кешировать запрос в List<>. /// /// /// /// Метка кеша /// Период устаревания данных /// public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence) where TEntity : class { object[] factory() => query.AsNoTracking().ToArray(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache.GetData(); } /// /// Кешировать запрос с последующим преобразованием из в .
/// Преобразование выполняется после получения из БД, результат кешируется в List<>. ///
/// /// /// /// Метка кеша /// Период устаревания данных /// Преобразование данных БД в DTO /// public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func convert) where TEntity : class { object[] factory() => query.AsNoTracking().ToArray(); var cache = GetOrAddCache(tag, factory, obsolescence); return cache.GetData(convert); } public static Task> FromCacheAsync(this IQueryable query, CancellationToken token = default) where TEntity : class { var tag = typeof(TEntity).Name; return FromCacheAsync(query, tag, defaultObsolescence, token); } /// /// Асинхронно кешировать запрос в List<>.
///
/// /// /// Метка кеша /// Период устаревания данных /// /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default) where TEntity : class { async Task factory(CancellationToken token) => await query.AsNoTracking().ToArrayAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache.GetData(); } /// /// Асинхронно кешировать запрос с последующим преобразованием из в .
/// Преобразование выполняется после получения из БД, результат кешируется в List<>. ///
/// /// /// /// Метка кеша /// Период устаревания данных /// Преобразование данных БД в DTO /// /// public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func convert, CancellationToken token = default) where TEntity : class { async Task factory(CancellationToken token) => await query.AsNoTracking().ToArrayAsync(token); var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token); return cache.GetData(convert); } /// /// drops cache with tag = typeof(T).Name /// /// /// public static void DropCache(this IQueryable query) { var tag = typeof(T).Name; DropCache(query, tag); } /// /// Очистить кеш /// /// /// /// Метка кеша public static void DropCache(this IQueryable query, string tag) { caches.Remove(tag, out var _); } } #nullable disable }