using Microsoft.Extensions.Caching.Memory; namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal; /// /// Кеширующая обёртка над . Резолв одного /// маршрута (TemplateRouteService.ResolveAsync) может дёргать справочник /// api.tnsu.kz по многу раз за один запрос — один и тот же согласующий, /// должность или руководитель нередко повторяется в разных шагах длинного /// маршрута, — и без кеша распараллеливание самих шагов резолва не спасает от /// нагрузки на api.tnsu.kz: он просто получает то же число запросов /// одновременно вместо по очереди. Кеш процессный (, /// не распределённый — реплик API несколько, поэтому в них он не общий), TTL /// фиксирован на . /// public sealed class CachingTnsuPortalClient(HttpTnsuPortalClient inner, IMemoryCache cache) : ITnsuPortalClient { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); public Task> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken) => GetOrAddAsync( $"emp:email:{email.ToLowerInvariant()}", () => inner.GetEmployeeByEmailAsync(email, cancellationToken)); public Task GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken) => GetOrAddAsync( $"emp:position:{position.ToLowerInvariant()}|{company.ToLowerInvariant()}", () => inner.GetEmployeeByPositionAsync(position, company, cancellationToken)); public Task> GetAllEmployeesAsync(CancellationToken cancellationToken) => GetOrAddAsync("emp:all", () => inner.GetAllEmployeesAsync(cancellationToken)); public Task GetManagerAsync(string email, string managerType, CancellationToken cancellationToken) => GetOrAddAsync( $"mgr:{email.ToLowerInvariant()}|{managerType.ToLowerInvariant()}", () => inner.GetManagerAsync(email, managerType, cancellationToken)); // Базы целиком (/Employee/All выше и /Employee/FullBase ниже) кешируем по // фиксированному ключу — это не запросы «по чему-то», а единственные на весь // процесс снимки справочника; TTL тот же (5 минут), что и у остальных // обращений к api.tnsu.kz. public Task> GetFullBaseAsync(CancellationToken cancellationToken) => GetOrAddAsync("emp:fullbase", () => inner.GetFullBaseAsync(cancellationToken)); public Task> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken) => GetOrAddAsync( $"projexec:{projectId}", () => inner.GetProjectExecutorsAsync(projectId, cancellationToken)); public Task GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken) => GetOrAddAsync( $"depthead:{departmentId}", () => inner.GetDepartmentHeadEmailAsync(departmentId, cancellationToken)); /// /// Кеширует и «не нашли» (null / пустой список) — повторять запрос к /// api.tnsu.kz за тем же результатом на каждый одинаковый шаг маршрута /// незачем. Ключ по одному и тому же значению может задеть гонку: два /// конкурентных шага (см. Task.WhenAll в ResolveAsync) с одинаковым ключом /// оба не найдут его в кеше и оба сходят в API — не страшно, это лишний /// один поход, а не порча данных; блокировка per-key усложнила бы код ради /// выигрыша, который при TTL в минуты и так почти всегда есть без неё. /// private async Task GetOrAddAsync(string key, Func> factory) { if (cache.TryGetValue(key, out T? cached)) return cached!; var value = await factory(); cache.Set(key, value, CacheDuration); return value; } }