2022-06-01 15:59:02 +05:00
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 ) ;
2022-06-10 17:32:05 +05:00
private static readonly TimeSpan defaultObsolescence = TimeSpan . FromMinutes ( 4 ) ;
2022-06-01 15:59:02 +05:00
private class CacheItem
{
internal IDictionary ? Data ;
internal DateTime DateObsolete ;
internal DateTime DateObsoleteTotal ;
internal readonly SemaphoreSlim semaphore = new ( 1 ) ;
2022-06-06 15:43:47 +05:00
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 ;
2022-06-15 14:57:37 +05:00
if ( Data is Dictionary < TKey , TEntity > typedEntityData )
2022-06-06 15:43:47 +05:00
{
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." ) ;
}
2022-06-01 15:59:02 +05:00
}
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 ;
}
2022-06-10 17:32:05 +05:00
/// <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 ) ;
}
2022-06-01 15:59:02 +05:00
/// <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 ( )
2022-06-06 15:43:47 +05:00
= > query . AsNoTracking ( ) . ToDictionary ( keySelector ) ;
2022-06-01 15:59:02 +05:00
var cache = GetOrAddCache ( tag , factory , obsolescence ) ;
2022-06-06 15:43:47 +05:00
return cache . GetData < TKey , TEntity > ( ) ;
2022-06-01 15:59:02 +05:00
}
/// <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 ( )
2022-06-06 15:43:47 +05:00
= > query . AsNoTracking ( ) . ToDictionary ( keySelector ) ;
2022-06-01 15:59:02 +05:00
var cache = GetOrAddCache ( tag , factory , obsolescence ) ;
2022-06-06 15:43:47 +05:00
return cache . GetData < TKey , TEntity , TModel > ( convert ) ;
2022-06-01 15:59:02 +05:00
}
2022-06-10 17:32:05 +05:00
/// <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 ) ;
}
2022-06-01 15:59:02 +05:00
/// <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 > (
2022-06-15 14:57:37 +05:00
this IQueryable < TEntity > query ,
string tag ,
TimeSpan obsolescence ,
Func < TEntity , TKey > keySelector ,
2022-06-01 15:59:02 +05:00
CancellationToken token = default )
where TEntity : class
where TKey : notnull
{
async Task < IDictionary > factory ( CancellationToken token )
2022-06-15 14:57:37 +05:00
= > await query . AsNoTracking ( ) . ToDictionaryAsync ( keySelector , token ) ;
2022-06-01 15:59:02 +05:00
var cache = await GetOrAddCacheAsync ( tag , factory , obsolescence , token ) ;
2022-06-06 15:43:47 +05:00
return cache . GetData < TKey , TEntity > ( ) ;
2022-06-01 15:59:02 +05:00
}
/// <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 )
2022-06-06 15:43:47 +05:00
= > await query . AsNoTracking ( ) . ToDictionaryAsync ( keySelector , token ) ;
2022-06-01 15:59:02 +05:00
var cache = await GetOrAddCacheAsync ( tag , factory , obsolescence , token ) ;
2022-06-06 15:43:47 +05:00
return cache . GetData < TKey , TEntity , TModel > ( convert ) ;
2022-06-01 15:59:02 +05:00
}
2022-06-10 17:32:05 +05:00
/// <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 ) ;
}
2022-06-01 15:59:02 +05:00
/// <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
}