forked from ddrilling/AsbCloudServer
440 lines
19 KiB
C#
440 lines
19 KiB
C#
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.
|
||
/// Кеш не отслеживается ChangeTracker.
|
||
/// </summary>
|
||
public static class EfCacheDictionaryExtensions
|
||
{
|
||
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 IDictionary? Data;
|
||
internal DateTime DateObsolete;
|
||
internal DateTime DateObsoleteTotal;
|
||
internal readonly SemaphoreSlim semaphore = new(1);
|
||
|
||
|
||
internal Dictionary<TKey, TEntity> GetData<TKey, TEntity>()
|
||
where TKey : notnull
|
||
{
|
||
if (Data is Dictionary<TKey, TEntity> typedData)
|
||
return typedData;
|
||
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
|
||
}
|
||
|
||
internal Dictionary<TKey, TModel> GetData<TKey, TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1)
|
||
where TKey : notnull
|
||
{
|
||
if (Data is Dictionary<TKey, TModel> typedData)
|
||
return typedData;
|
||
if (Data is Dictionary<TKey, TEntity > typedEntityData)
|
||
{
|
||
if (semaphore.Wait(0))
|
||
{
|
||
try
|
||
{
|
||
var convertedData = typedEntityData.ToDictionary(i => i.Key, i => convert(i.Value));
|
||
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<TKey, TEntity, TModel>(convert, --attempt);
|
||
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
|
||
}
|
||
}
|
||
|
||
private static CacheItem GetOrAddCache(string tag, Func<IDictionary> 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<IDictionary>> 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>
|
||
/// Кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом typeof(TEntity).Name и ключом int Id
|
||
/// </summary>
|
||
/// <typeparam name="TEntity"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <returns></returns>
|
||
public static Dictionary<int, TEntity> FromCacheDictionary<TEntity>(
|
||
this IQueryable<TEntity> query)
|
||
where TEntity : class, AsbCloudDb.Model.IId
|
||
{
|
||
var tag = typeof(TEntity).Name;
|
||
return FromCacheDictionary(query, tag, defaultObsolescence, e => e.Id);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом typeof(TEntity).Name
|
||
/// </summary>
|
||
/// <typeparam name="TKey"></typeparam>
|
||
/// <typeparam name="TEntity"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <returns></returns>
|
||
public static Dictionary<TKey, TEntity> FromCacheDictionary<TKey, TEntity>(
|
||
this IQueryable<TEntity> query,
|
||
Func<TEntity, TKey> keySelector)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
var tag = typeof(TEntity).Name;
|
||
return FromCacheDictionary(query, tag, defaultObsolescence, keySelector);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="TEntity">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <returns></returns>
|
||
public static Dictionary<TKey, TEntity> FromCacheDictionary<TKey, TEntity>(
|
||
this IQueryable<TEntity> query,
|
||
string tag,
|
||
TimeSpan obsolescence,
|
||
Func<TEntity, TKey> keySelector)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
IDictionary factory()
|
||
=> query.AsNoTracking().ToDictionary(keySelector);
|
||
var cache = GetOrAddCache(tag, factory, obsolescence);
|
||
return cache.GetData<TKey, TEntity>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
|
||
/// Преобразование выполняется после получения из БД, результат кешируется в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TModel"/>>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="TEntity">тип значения</typeparam>
|
||
/// <typeparam name="TModel"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="convert">Преобразование данных БД в DTO</param>
|
||
/// <returns></returns>
|
||
public static Dictionary<TKey, TModel> FromCacheDictionary<TKey, TEntity, TModel>(
|
||
this IQueryable<TEntity> query,
|
||
string tag,
|
||
TimeSpan obsolescence,
|
||
Func<TEntity, TKey> keySelector,
|
||
Func<TEntity, TModel> convert)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
IDictionary factory()
|
||
=> query.AsNoTracking().ToDictionary(keySelector);
|
||
var cache = GetOrAddCache(tag, factory, obsolescence);
|
||
return cache.GetData<TKey, TEntity, TModel>(convert);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом typeof(TEntity).Name и ключом int Id
|
||
/// </summary>
|
||
/// <typeparam name="TEntity"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
public static Task<Dictionary<int, TEntity>> FromCacheDictionaryAsync<TEntity>(
|
||
this IQueryable<TEntity> query,
|
||
CancellationToken token = default)
|
||
where TEntity : class, AsbCloudDb.Model.IId
|
||
{
|
||
var tag = typeof(TEntity).Name;
|
||
return FromCacheDictionaryAsync(query, tag, defaultObsolescence, e => e.Id, token);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом typeof(TEntity).Name
|
||
/// </summary>
|
||
/// <typeparam name="TKey"></typeparam>
|
||
/// <typeparam name="TEntity"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
public static Task<Dictionary<TKey, TEntity>> FromCacheDictionaryAsync<TKey, TEntity>(
|
||
this IQueryable<TEntity> query,
|
||
Func<TEntity, TKey> keySelector,
|
||
CancellationToken token = default)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
var tag = typeof(TEntity).Name;
|
||
return FromCacheDictionaryAsync(query, tag, defaultObsolescence, keySelector, token);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="TEntity">тип значения</typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
public static async Task<Dictionary<TKey, TEntity>> FromCacheDictionaryAsync<TKey, TEntity>(
|
||
this IQueryable<TEntity> query,
|
||
string tag,
|
||
TimeSpan obsolescence,
|
||
Func<TEntity, TKey> keySelector,
|
||
CancellationToken token = default)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
async Task<IDictionary> factory(CancellationToken token)
|
||
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
|
||
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
|
||
return cache.GetData<TKey, TEntity>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
|
||
/// Преобразование выполняется после получения из БД, результат кешируется в Dictionary<<typeparamref name="TKey"/>, <typeparamref name="TModel"/>>.
|
||
/// </summary>
|
||
/// <typeparam name="TKey">тип ключа</typeparam>
|
||
/// <typeparam name="TEntity">тип значения</typeparam>
|
||
/// <typeparam name="TModel"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
/// <param name="obsolescence">Период устаревания данных</param>
|
||
/// <param name="keySelector">Делегат получения ключа из записи</param>
|
||
/// <param name="convert">Преобразование данных БД в DTO</param>
|
||
/// <param name="token"></param>
|
||
/// <returns></returns>
|
||
public static async Task<Dictionary<TKey, TModel>> FromCacheDictionaryAsync<TKey, TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TKey> keySelector, Func<TEntity, TModel> convert, CancellationToken token = default)
|
||
where TEntity : class
|
||
where TKey : notnull
|
||
{
|
||
async Task<IDictionary> factory(CancellationToken token)
|
||
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
|
||
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
|
||
return cache.GetData<TKey, TEntity, TModel>(convert);
|
||
}
|
||
|
||
/// <summary>
|
||
/// drops cache with tag = typeof(T).Name
|
||
/// </summary>
|
||
/// <typeparam name="T"></typeparam>
|
||
/// <param name="query"></param>
|
||
public static void DropCacheDictionary<T>(this IQueryable<T> query)
|
||
{
|
||
var tag = typeof(T).Name;
|
||
DropCacheDictionary<T>(query, tag);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Очистить кеш
|
||
/// </summary>
|
||
/// <typeparam name="T"></typeparam>
|
||
/// <param name="query"></param>
|
||
/// <param name="tag">Метка кеша</param>
|
||
public static void DropCacheDictionary<T>(this IQueryable<T> query, string tag)
|
||
{
|
||
caches.Remove(tag, out var _);
|
||
}
|
||
}
|
||
#nullable disable
|
||
}
|