using Microsoft.EntityFrameworkCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services.Cache
{
#nullable enable
///
/// Кеширование запросов EF.
///
public static class EfCacheL2
{
///
/// Кол-во обращений к БД.
///
public static int CountOfRequestsToDB = 0;
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 class CacheItem
{
internal IEnumerable? Data;
internal DateTime DateObsolete;
internal DateTime DateObsoleteTotal;
internal readonly SemaphoreSlim semaphore = new(1);
}
private static CacheItem GetOrAddCache(string tag, Func valueFactory, TimeSpan obsolete)
{
CacheItem cache;
while (!caches.ContainsKey(tag))
{
if (semaphore.Wait(0))
{
try {
cache = new CacheItem();
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;
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 GetOrAddCacheAsync(string tag, Func> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
{
CacheItem cache;
while (!caches.ContainsKey(tag))
{
if (semaphore.Wait(0))
{
try
{
cache = new CacheItem();
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;
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;
}
private static IEnumerable ConvertToIEnumerable(IEnumerable? data)
{
if (data is IEnumerable list)
return list;
else if (data is IDictionary dictionary)
{
System.Diagnostics.Trace.TraceWarning($"ConvertToIEnumerable. Use keyless method on keyed cache. Type: {typeof(T).Name};");
return (IEnumerable)dictionary.Values;
}
else
throw new NotSupportedException("cache.Data has wrong type.");
}
private static Dictionary ConvertToDictionary(IEnumerable? data, Func keySelector)
where TKey : notnull
{
if (data is Dictionary dictionary)
return dictionary;
else if (data is IEnumerable enumerable)
{
System.Diagnostics.Trace.TraceWarning($"ConvertToDictionary. Use keyed method on keyless cache. Type: {typeof(T).Name};");
return enumerable.ToDictionary(keySelector);
}
else
throw new NotSupportedException("cache.Data has wrong type.");
}
///
/// Кешировать запрос в List\.
/// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
///
///
///
/// Метка кеша
/// Период устаревания данных
///
public static IEnumerable FromCache(this IQueryable query, string tag, TimeSpan obsolescence)
{
IEnumerable factory ()
{
CountOfRequestsToDB++;
return query.ToList();
}
var cache = GetOrAddCache(tag, factory, obsolescence);
return ConvertToIEnumerable(cache.Data);
}
///
/// Асинхронно кешировать запрос в List\.
/// Выборки по PK будут работать медленнее, чем при кешировании в виде словаря.
///
///
///
/// Метка кеша
/// Период устаревания данных
///
///
public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, CancellationToken token = default)
{
async Task factory(CancellationToken token)
{
CountOfRequestsToDB++;
return await query.ToListAsync(token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return ConvertToIEnumerable(cache.Data);
}
///
/// Кешировать запрос в Dictionary\.
///
/// тип ключа
/// тип значения
///
/// Метка кеша
/// Период устаревания данных
/// Делегат получения ключа из записи
///
///
///
///
public static Dictionary FromCache(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector)
where TKey: notnull
{
IEnumerable factory ()
{
CountOfRequestsToDB++;
return query.ToDictionary(keySelector);
}
var cache = GetOrAddCache(tag, factory, obsolescence);
return ConvertToDictionary(cache.Data, keySelector);
}
///
/// Асинхронно кешировать запрос в Dictionary\.
///
/// тип ключа
/// тип значения
///
/// Метка кеша
/// Период устаревания данных
/// Делегат получения ключа из записи
///
///
public static async Task> FromCacheAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, CancellationToken token = default)
where TKey : notnull
{
async Task factory(CancellationToken token)
{
CountOfRequestsToDB++;
return await query.ToDictionaryAsync(keySelector, token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return ConvertToDictionary(cache.Data, keySelector);
}
///
/// Получить запись из кеша по ключу.
/// При отсутствии кеша создаст его для всех записей из query.
///
/// тип ключа
/// тип значения
///
/// Метка кеша
/// Период устаревания данных
/// Делегат получения ключа из записи
///
///
/// if cache contains trash
public static T? FromCacheGetValueOrDefault(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key)
where TKey : notnull
{
IEnumerable factory()
{
CountOfRequestsToDB++;
return query.ToDictionary(keySelector);
}
var cache = GetOrAddCache(tag, factory, obsolescence);
var data = cache.Data;
if (data is Dictionary dictionary)
return dictionary.GetValueOrDefault(key);
else if (data is IEnumerable enumerable)
{
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
}
else
throw new NotSupportedException("cache.Data has wrong type.");
}
///
/// Асинхронно получить запись из кеша по ключу.
/// При отсутствии кеша создаст его для всех записей из query.
///
/// тип ключа
/// тип значения
///
/// Метка кеша
/// Период устаревания данных
/// Делегат получения ключа из записи
///
///
///
/// if cache contains trash
public static async Task FromCacheGetValueOrDefaultAsync(this IQueryable query, string tag, TimeSpan obsolescence, Func keySelector, TKey key, CancellationToken token = default)
where TKey : notnull
{
async Task factory(CancellationToken token)
{
CountOfRequestsToDB++;
return await query.ToDictionaryAsync(keySelector, token);
}
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
var data = cache.Data;
if (data is Dictionary dictionary)
return dictionary.GetValueOrDefault(key);
else if (data is IEnumerable enumerable)
{
System.Diagnostics.Trace.TraceWarning($"Use keyed method on keyless cache. Tag: {tag}, type: {typeof(T).Name};");
return enumerable.FirstOrDefault(v => keySelector(v).Equals(key));
}
else
throw new NotSupportedException("cache.Data has wrong type.");
}
public static void DropCache(this IQueryable query, string tag)
{
caches.Remove(tag, out var _);
}
}
#nullable disable
}