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

388 lines
15 KiB
C#
Raw Normal View History

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.<br/>
/// Кеш не отслеживается ChangeTracker.
/// </summary>
public static class EfCacheExtensions
{
2022-06-15 14:57:37 +05:00
private static readonly Dictionary<string, CacheItem> caches = new(16);
2022-06-01 15:59:02 +05:00
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);
2022-06-01 15:59:02 +05:00
2022-10-28 09:05:33 +05:00
private class YeldConvertedData<TEntity, TModel> : IEnumerable<TModel>
{
IEnumerable data;
public YeldConvertedData(IEnumerable<TEntity> entities, Func<TEntity, TModel> convert)
{
data = entities;
}
class Enumerator : IEnumerator<TModel>
{
private readonly IEnumerable data;
public Enumerator( IEnumerable data, Func<TEntity, TModel> convert)
{
this.data = data;
}
public TModel Current => throw new NotImplementedException();
object IEnumerator.Current => throw new NotImplementedException();
public void Dispose()
{
throw new NotImplementedException();
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}
public IEnumerator<TModel> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
2022-06-01 15:59:02 +05:00
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
{
2022-10-28 09:05:33 +05:00
var convertedData = typedEntityData.Select(convert).ToArray();
Data = convertedData;
return convertedData;
}
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");
}
}
}
2022-06-15 14:57:37 +05:00
if (attempt > 0)
return GetData(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<IEnumerable> valueFactory, TimeSpan obsolete)
{
CacheItem cache;
while (!caches.ContainsKey(tag))
{
if (semaphore.Wait(0))
{
2022-06-15 14:57:37 +05:00
try
{
2022-06-01 15:59:02 +05:00
cache = new CacheItem();
caches.Add(tag, cache);
}
finally
{
semaphore.Release();
2022-06-15 14:57:37 +05:00
}
2022-06-01 15:59:02 +05:00
break;
}
else
{
if (semaphore.Wait(semaphoreTimeout))
{
semaphore.Release();
}
else
{
semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
}
2022-06-15 14:57:37 +05:00
}
2022-06-01 15:59:02 +05:00
}
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;
}
finally
{
cache.semaphore.Release();
}
}
2022-06-15 14:57:37 +05:00
else if (cache.DateObsoleteTotal < DateTime.Now)
2022-06-01 15:59:02 +05:00
{
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");
}
2022-06-15 14:57:37 +05:00
}
2022-06-01 15:59:02 +05:00
}
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);
}
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;
}
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);
}
2022-06-01 15:59:02 +05:00
/// <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();
2022-06-01 15:59:02 +05:00
var cache = GetOrAddCache(tag, factory, obsolescence);
return cache.GetData<TEntity>();
2022-06-01 15:59:02 +05:00
}
/// <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
{
2022-06-15 14:57:37 +05:00
IEnumerable factory() => query.AsNoTracking().ToList();
2022-06-01 15:59:02 +05:00
var cache = GetOrAddCache(tag, factory, obsolescence);
return cache.GetData(convert);
2022-06-01 15:59:02 +05:00
}
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);
}
2022-06-01 15:59:02 +05:00
/// <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);
2022-06-01 15:59:02 +05:00
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return cache.GetData<TEntity>();
2022-06-01 15:59:02 +05:00
}
/// <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)
2022-06-15 14:57:37 +05:00
=> await query.AsNoTracking().ToListAsync(token);
2022-06-01 15:59:02 +05:00
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return cache.GetData(convert);
2022-06-01 15:59:02 +05:00
}
/// <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);
}
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 DropCache<T>(this IQueryable<T> query, string tag)
{
caches.Remove(tag, out var _);
}
}
#nullable disable
}