DD.WellWorkover.Cloud/AsbCloudInfrastructure/EfCache/EfCacheExtensions.cs

359 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4);
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();
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();
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&lt;<typeparamref name="TEntity"/>&gt;. Кеш tag = typeof(TEntity).Name
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <returns></returns>
public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query)
where TEntity : class
{
var tag = typeof(TEntity).Name;
return FromCache(query, tag, defaultObsolescence);
}
/// <summary>
/// Кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.
/// </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&lt;<typeparamref name="TEntity"/>&gt;.
/// </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);
}
public static Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, CancellationToken token = default)
where TEntity : class
{
var tag = typeof(TEntity).Name;
return FromCacheAsync(query, tag, defaultObsolescence, token);
}
/// <summary>
/// Асинхронно кешировать запрос в List&lt;<typeparamref name="TEntity"/>&gt;.<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&lt;<typeparamref name="TModel"/>&gt;.
/// </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>
/// drops cache with tag = typeof(T).Name
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
public static void DropCache<T>(this IQueryable<T> query)
{
var tag = typeof(T).Name;
DropCache(query, tag);
}
/// <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
}