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 : IEnumerable 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 cached; private readonly DbSet dbSet; internal CacheTable(DbContext context, (DateTime refreshDate, IEnumerable entities) data) { this.context = context; this.data = data; dbSet = context.Set(); cached = (List)data.entities; if (cached.Count == 0) Refresh(); } public TEntity this[int index] { get => cached.ElementAt(index); } /// /// Runs action like atomic operation. /// wasFree is action argument indicates that semaphore was free. /// It may be needed to avoid multiple operations like Refresh(). /// /// (wasFree) => {...} /// false - semaphore.Wait returned by timeout private static bool Sync(Action 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; } /// /// Runs action like atomic operation. /// wasFree is action argument indicates that semaphore was free. /// It may be needed to avoid multiple operations like Refresh(). /// /// (wasFree) => {...} /// false - semaphore.Wait returned by timeout private static async Task SyncAsync(Func 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().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 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 predicate) => FirstOrDefault(predicate) != default; public async Task ContainsAsync(Func predicate, CancellationToken token = default) => await FirstOrDefaultAsync(predicate, token) != default; public TEntity GetOrCreate(Func predicate, Func 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 FirstOrDefaultAsync(CancellationToken token = default) { var result = cached.FirstOrDefault(); if (result != default) return result; await RefreshAsync(token); return cached.FirstOrDefault(); } public TEntity FirstOrDefault(Func predicate) { var result = cached.FirstOrDefault(predicate); if (result != default) return result; Refresh(); return cached.FirstOrDefault(predicate); } public async Task FirstOrDefaultAsync(Func predicate, CancellationToken token = default) { var result = cached.FirstOrDefault(predicate); if (result != default) return result; await RefreshAsync(token); return cached.FirstOrDefault(predicate); } public IEnumerable Where(Func 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> WhereAsync(CancellationToken token = default) => WhereAsync(default, token); public async Task> WhereAsync(Func 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 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 entities, CancellationToken token = default) { if (!entities.Any()) return; await SyncAsync(async (wasFree, token) => { var upsertedEntries = new List(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 predicate) => Sync(_ => { dbSet.RemoveRange(dbSet.Where(predicate)); context.SaveChanges(); InternalRefresh(); }); public Task RemoveAsync(Func 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 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 newEntities) { int result = 0; Sync(_ => { dbSet.AddRange(newEntities); result = context.SaveChanges(); InternalRefresh(); }); return result; } public async Task InsertAsync(IEnumerable 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 GetEnumerator() => Where().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } }