using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections; using System.Diagnostics; namespace AsbCloudInfrastructure.Services.Cache { public class CacheTable<TEntity> : IEnumerable<TEntity> where TEntity : class { private const int semaphoreTimeout = 5_000; private static readonly SemaphoreSlim semaphore = new(1); private readonly DbContext context; private (DateTime refreshDate, IEnumerable entities) data; private readonly List<TEntity> cached; private readonly DbSet<TEntity> dbSet; internal CacheTable(DbContext context, (DateTime refreshDate, IEnumerable entities) data) { this.context = context; this.data = data; dbSet = context.Set<TEntity>(); cached = (List<TEntity>)data.entities; if (cached.Count == 0) Refresh(); } public TEntity this[int index] { get => cached.ElementAt(index); } /// <summary> /// Runs action like atomic operation. /// wasFree is action argument indicates that semaphore was free. /// It may be needed to avoid multiple operations like Refresh(). /// </summary> /// <param name="action">(wasFree) => {...}</param> /// <returns>false - semaphore.Wait returned by timeout</returns> private static bool Sync(Action<bool> action) { var wasFree = semaphore.CurrentCount > 0; if (!semaphore.Wait(semaphoreTimeout)) return false; try { action?.Invoke(wasFree); } catch (Exception ex) { Trace.WriteLine($"{DateTime.Now:yyyy.MM.dd HH:mm:ss:fff} error in CacheTable<{typeof(TEntity).Name}>.Sync()"); Trace.WriteLine(ex.Message); Trace.WriteLine(ex.StackTrace); } finally { semaphore.Release(); } return true; } /// <summary> /// Runs action like atomic operation. /// wasFree is action argument indicates that semaphore was free. /// It may be needed to avoid multiple operations like Refresh(). /// </summary> /// <param name="action">(wasFree) => {...}</param> /// <returns>false - semaphore.Wait returned by timeout</returns> private static async Task<bool> SyncAsync(Func<bool, CancellationToken, Task> task, CancellationToken token = default) { var wasFree = semaphore.CurrentCount > 0; if (!await semaphore.WaitAsync(semaphoreTimeout, token).ConfigureAwait(false)) return false; try { await task?.Invoke(wasFree, token); } catch (Exception ex) { Trace.WriteLine($"{DateTime.Now:yyyy.MM.dd HH:mm:ss:fff} error in CacheTable<{typeof(TEntity).Name}>.SyncAsync()"); Trace.WriteLine(ex.Message); Trace.WriteLine(ex.StackTrace); } finally { semaphore.Release(); } return true; } private void InternalRefresh() { cached.Clear(); var entities = dbSet.AsNoTracking().ToList(); cached.AddRange(entities); data.refreshDate = DateTime.Now; } private async Task InternalRefreshAsync(CancellationToken token = default) { cached.Clear(); var entities = await context.Set<TEntity>().AsNoTracking() .ToListAsync(token).ConfigureAwait(false); cached.AddRange(entities); data.refreshDate = DateTime.Now; } public int Refresh() { Sync((wasFree) => { if (wasFree) InternalRefresh(); }); return cached.Count; } public async Task<int> RefreshAsync(CancellationToken token = default) { await SyncAsync(async (wasFree, token) => { if (wasFree) await InternalRefreshAsync(token).ConfigureAwait(false); }, token).ConfigureAwait(false); return cached.Count; } public bool Contains(Func<TEntity, bool> predicate) => FirstOrDefault(predicate) != default; public async Task<bool> ContainsAsync(Func<TEntity, bool> predicate, CancellationToken token = default) => await FirstOrDefaultAsync(predicate, token) != default; public TEntity GetOrCreate(Func<TEntity, bool> predicate, Func<TEntity> makeNew) { TEntity result = default; Sync(wasFree => { result = cached.FirstOrDefault(predicate); if (result != default) return; InternalRefresh(); result = cached.FirstOrDefault(predicate); if (result != default) return; var entry = dbSet.Add(makeNew()); context.SaveChanges(); InternalRefresh(); result = entry.Entity; }); return result; } public TEntity FirstOrDefault() { var result = cached.FirstOrDefault(); if (result != default) return result; Refresh(); return cached.FirstOrDefault(); } public async Task<TEntity> FirstOrDefaultAsync(CancellationToken token = default) { var result = cached.FirstOrDefault(); if (result != default) return result; await RefreshAsync(token); return cached.FirstOrDefault(); } public TEntity FirstOrDefault(Func<TEntity, bool> predicate) { var result = cached.FirstOrDefault(predicate); if (result != default) return result; Refresh(); return cached.FirstOrDefault(predicate); } public async Task<TEntity> FirstOrDefaultAsync(Func<TEntity, bool> predicate, CancellationToken token = default) { var result = cached.FirstOrDefault(predicate); if (result != default) return result; await RefreshAsync(token); return cached.FirstOrDefault(predicate); } public IEnumerable<TEntity> Where(Func<TEntity, bool> predicate = default) { var result = (predicate != default) ? cached.Where(predicate) : cached; if (result.Any()) return result; Refresh(); result = (predicate != default) ? cached.Where(predicate) : cached; return result; } public Task<IEnumerable<TEntity>> WhereAsync(CancellationToken token = default) => WhereAsync(default, token); public async Task<IEnumerable<TEntity>> WhereAsync(Func<TEntity, bool> predicate = default, CancellationToken token = default) { var result = (predicate != default) ? cached.Where(predicate) : cached; if (result.Any()) return result; await RefreshAsync(token); result = (predicate != default) ? cached.Where(predicate) : cached; return result; } public void Upsert(TEntity entity) { if (entity == default) return; Sync((wasFree) => { if (dbSet.Contains(entity)) dbSet.Update(entity); else dbSet.Add(entity); context.SaveChanges(); InternalRefresh(); }); } public Task UpsertAsync(TEntity entity, CancellationToken token = default) => SyncAsync(async (wasFree, token) => { if (dbSet.Contains(entity)) dbSet.Update(entity); else dbSet.Add(entity); await context.SaveChangesAsync(token).ConfigureAwait(false); await InternalRefreshAsync(token).ConfigureAwait(false); }, token); public void Upsert(IEnumerable<TEntity> entities) { if (!entities.Any()) return; Sync((wasFree) => { foreach (var entity in entities) { if (dbSet.Contains(entity)) // TODO: это очень ммедленно dbSet.Update(entity); else dbSet.Add(entity); } context.SaveChanges(); InternalRefresh(); }); } public async Task UpsertAsync(IEnumerable<TEntity> entities, CancellationToken token = default) { if (!entities.Any()) return; await SyncAsync(async (wasFree, token) => { var upsertedEntries = new List<TEntity>(entities.Count()); foreach (var entity in entities) { if (dbSet.Contains(entity)) dbSet.Update(entity); else dbSet.Add(entity); } await context.SaveChangesAsync(token).ConfigureAwait(false); await InternalRefreshAsync(token).ConfigureAwait(false); }, token); } public void Remove(Func<TEntity, bool> predicate) => Sync(_ => { dbSet.RemoveRange(dbSet.Where(predicate)); context.SaveChanges(); InternalRefresh(); }); public Task RemoveAsync(Func<TEntity, bool> predicate, CancellationToken token = default) => SyncAsync(async (wasFree, token) => { dbSet.RemoveRange(dbSet.Where(predicate)); await context.SaveChangesAsync(token).ConfigureAwait(false); await InternalRefreshAsync(token).ConfigureAwait(false); }, token); public TEntity Insert(TEntity entity) { TEntity result = default; Sync(_ => { var entry = dbSet.Add(entity); context.SaveChanges(); InternalRefresh(); result = entry.Entity; }); return result; } public async Task<TEntity> InsertAsync(TEntity entity, CancellationToken token = default) { TEntity result = default; await SyncAsync(async (wasFree, token) => { var entry = dbSet.Add(entity); await context.SaveChangesAsync(token).ConfigureAwait(false); await InternalRefreshAsync(token).ConfigureAwait(false); result = entry.Entity; }, token); return result; } public int Insert(IEnumerable<TEntity> newEntities) { int result = 0; Sync(_ => { dbSet.AddRange(newEntities); result = context.SaveChanges(); InternalRefresh(); }); return result; } public async Task<int> InsertAsync(IEnumerable<TEntity> newEntities, CancellationToken token = default) { int result = 0; await SyncAsync(async (wasFree, token) => { dbSet.AddRange(newEntities); result = await context.SaveChangesAsync(token).ConfigureAwait(false); await RefreshAsync(token).ConfigureAwait(false); }, token); return result; } public IEnumerator<TEntity> GetEnumerator() => Where().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } }