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

440 lines
19 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.
/// Кеш не отслеживается 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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом 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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом 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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
/// </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&lt;<typeparamref name="TKey"/>, <typeparamref name="TModel"/>&gt;.
/// </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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом 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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом 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&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
/// </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&lt;<typeparamref name="TKey"/>, <typeparamref name="TModel"/>&gt;.
/// </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
}