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);
|
2022-06-10 17:32:05 +05:00
|
|
|
|
private static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4);
|
2022-06-01 15:59:02 +05:00
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
private class YieldConvertedData<TEntity, TModel> : IEnumerable<TModel>
|
2022-10-28 09:05:33 +05:00
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
private struct ConvertedData
|
2022-10-28 09:05:33 +05:00
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
public TEntity? Entity;
|
|
|
|
|
public TModel? Model;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
ConvertedData[] data;
|
|
|
|
|
public Func<TEntity, TModel> convert { get; }
|
|
|
|
|
|
|
|
|
|
public YieldConvertedData(TEntity[] entities, Func<TEntity, TModel> convert)
|
|
|
|
|
{
|
|
|
|
|
data = (entities.Select(x => new ConvertedData {
|
|
|
|
|
Entity = x,
|
|
|
|
|
Model = default }))
|
|
|
|
|
.ToArray();
|
|
|
|
|
this.convert = convert;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class YieldConvertedDataEnumerator : IEnumerator<TModel>
|
2022-10-28 09:05:33 +05:00
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
private readonly ConvertedData[] data;
|
|
|
|
|
private readonly Func<TEntity, TModel> convert;
|
|
|
|
|
private int position = -1;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
public YieldConvertedDataEnumerator(ConvertedData[] data, Func<TEntity, TModel> convert)
|
2022-10-28 09:05:33 +05:00
|
|
|
|
{
|
|
|
|
|
this.data = data;
|
2022-11-02 15:04:46 +05:00
|
|
|
|
this.convert = convert;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
public TModel Current
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (data[position].Entity is TEntity entity)
|
|
|
|
|
{
|
|
|
|
|
var dto = convert(entity);
|
|
|
|
|
data[position].Entity = default;
|
|
|
|
|
data[position].Model = dto;
|
|
|
|
|
}
|
|
|
|
|
return data[position].Model!;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-28 09:05:33 +05:00
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
object IEnumerator.Current => Current!;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool MoveNext()
|
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
position++;
|
|
|
|
|
return (position < data.Length);
|
2022-10-28 09:05:33 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Reset()
|
2022-11-02 15:04:46 +05:00
|
|
|
|
{
|
|
|
|
|
position = -1;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IEnumerator<TModel> GetEnumerator()
|
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
var result = new YieldConvertedDataEnumerator(data, convert);
|
|
|
|
|
return result;
|
2022-10-28 09:05:33 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2022-06-06 15:43:47 +05:00
|
|
|
|
|
|
|
|
|
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-11-02 15:04:46 +05:00
|
|
|
|
var convertedData = new YieldConvertedData<TEntity, TModel>(typedEntityData.ToArray(), convert);
|
2022-06-06 15:43:47 +05:00
|
|
|
|
Data = convertedData;
|
|
|
|
|
return convertedData;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
semaphore.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (semaphore.Wait(semaphoreTimeout))
|
|
|
|
|
{
|
|
|
|
|
semaphore.Release();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
semaphore.Release();
|
2022-11-02 15:04:46 +05:00
|
|
|
|
throw new TimeoutException("EfCacheL2.GetData. Can't wait too long while converting cache data");
|
2022-06-06 15:43:47 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-15 14:57:37 +05:00
|
|
|
|
if (attempt > 0)
|
2022-06-06 15:43:47 +05:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
private static CacheItem GetOrAddCache(string tag, Func<object[]> valueFactory, TimeSpan obsolete)
|
2022-06-01 15:59:02 +05:00
|
|
|
|
{
|
|
|
|
|
CacheItem cache;
|
|
|
|
|
while (!caches.ContainsKey(tag))
|
|
|
|
|
{
|
|
|
|
|
if (semaphore.Wait(0))
|
|
|
|
|
{
|
2022-06-15 14:57:37 +05:00
|
|
|
|
try
|
|
|
|
|
{
|
2022-11-03 13:58:40 +05:00
|
|
|
|
if (!caches.ContainsKey(tag))
|
|
|
|
|
{
|
|
|
|
|
cache = new CacheItem();
|
|
|
|
|
caches.Add(tag, cache);
|
|
|
|
|
}
|
2022-06-01 15:59:02 +05:00
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 15:04:46 +05:00
|
|
|
|
private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<object[]>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
|
2022-06-01 15:59:02 +05:00
|
|
|
|
{
|
|
|
|
|
CacheItem cache;
|
|
|
|
|
while (!caches.ContainsKey(tag))
|
|
|
|
|
{
|
|
|
|
|
if (semaphore.Wait(0))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2022-11-03 13:58:40 +05:00
|
|
|
|
if (!caches.ContainsKey(tag))
|
|
|
|
|
{
|
|
|
|
|
cache = new CacheItem();
|
|
|
|
|
caches.Add(tag, cache);
|
|
|
|
|
}
|
2022-06-01 15:59:02 +05:00
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 17:32:05 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Кешировать запрос в List<<typeparamref name="TEntity"/>>. Кеш 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<<typeparamref name="TEntity"/>>.
|
|
|
|
|
/// </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
|
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
object[] factory() => query.AsNoTracking().ToArray();
|
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<TEntity>();
|
2022-06-01 15:59:02 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
|
|
|
|
|
/// Преобразование выполняется после получения из БД, результат кешируется в List<<typeparamref name="TEntity"/>>.
|
|
|
|
|
/// </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-11-02 15:04:46 +05:00
|
|
|
|
object[] factory() => query.AsNoTracking().ToArray();
|
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(convert);
|
2022-06-01 15:59:02 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 17:07:47 +05:00
|
|
|
|
public static Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, CancellationToken token)
|
2022-06-10 17:32:05 +05:00
|
|
|
|
where TEntity : class
|
|
|
|
|
{
|
|
|
|
|
var tag = typeof(TEntity).Name;
|
|
|
|
|
return FromCacheAsync(query, tag, defaultObsolescence, token);
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-01 15:59:02 +05:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Асинхронно кешировать запрос в List<<typeparamref name="TEntity"/>>.<br/>
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="TEntity"></typeparam>
|
|
|
|
|
/// <param name="query"></param>
|
|
|
|
|
/// <param name="tag">Метка кеша</param>
|
|
|
|
|
/// <param name="obsolescence">Период устаревания данных</param>
|
|
|
|
|
/// <param name="token"></param>
|
|
|
|
|
/// <returns></returns>
|
2022-11-16 17:07:47 +05:00
|
|
|
|
public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token)
|
2022-06-01 15:59:02 +05:00
|
|
|
|
where TEntity : class
|
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
async Task<object[]> factory(CancellationToken token)
|
|
|
|
|
=> await query.AsNoTracking().ToArrayAsync(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<TEntity>();
|
2022-06-01 15:59:02 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
|
|
|
|
|
/// Преобразование выполняется после получения из БД, результат кешируется в List<<typeparamref name="TModel"/>>.
|
|
|
|
|
/// </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>
|
2022-11-16 17:07:47 +05:00
|
|
|
|
public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token)
|
2022-06-01 15:59:02 +05:00
|
|
|
|
where TEntity : class
|
|
|
|
|
{
|
2022-11-02 15:04:46 +05:00
|
|
|
|
async Task<object[]> factory(CancellationToken token)
|
|
|
|
|
=> await query.AsNoTracking().ToArrayAsync(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(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 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
|
|
|
|
|
}
|