using System; using System.Collections.Generic; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Client; using GB5Shared.Cache; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace GB5Shared.DALCache { /// /// DAL-level cache backed by the same Dapr state store used by the SL endpoint cache. /// Keys are prefixed with "dal:" to isolate DAL entries from SL endpoint responses /// stored under identical key formulas in the same Redis-backed store. /// /// Usage pattern — read-through: /// var key = new CacheKeyGeneration().KeyGeneration(id, EntityConstant.OBJECTFOO, CacheKeyLevel.CLIENT_LEVEL, login); /// var result = await _dalCache.GetOrSetAsync<FooDTO>(key, async ct => await _qe.QuerySingleAsync<FooDTO>(...), DALCache.TtlForLevel(CacheKeyLevel.CLIENT_LEVEL), ct); /// /// Usage pattern — invalidation (BLL, after write): /// var key = new CacheKeyGeneration().KeyGeneration(id, EntityConstant.OBJECTFOO, CacheKeyLevel.CLIENT_LEVEL, login); /// await _dalCache.InvalidateAsync(key, ct); /// public class DALCache : IDALCache { private readonly DaprClient _daprClient; private readonly ILogger _logger; private static readonly CacheLockRegistry _locks = new(); private const string StateStore = "statestore"; private const string KeyPrefix = "dal:"; public DALCache(DaprClient daprClient, ILogger logger) { _daprClient = daprClient; _logger = logger; } /// TTL in seconds for each cache level — mirrors BaseEndpoint.TtlForLevel. public static int TtlForLevel(int level) => level switch { CacheKeyLevel.OVERALL => 3600, CacheKeyLevel.DB_SERVER => 1800, CacheKeyLevel.CLIENT_LEVEL => 300, CacheKeyLevel.ROLE_LEVEL => 300, CacheKeyLevel.USER_LEVEL => 60, _ => 60 }; public async Task GetOrSetAsync( string cacheKey, Func> factory, int ttlSeconds, CancellationToken ct = default) where T : class { var dalKey = $"{KeyPrefix}{cacheKey}"; var sem = _locks.GetLock(dalKey); var acquired = await sem.WaitAsync(CacheLockRegistry.TimeoutMs, ct).ConfigureAwait(false); try { // Double-check after acquiring — another request may have populated while waiting. // A Dapr/statestore outage here must not fail the request — fall through to a live read. try { var cached = await _daprClient.GetStateAsync(StateStore, dalKey, cancellationToken: ct).ConfigureAwait(false); if (cached is not null) return JsonSerializer.Deserialize(cached); } catch (Exception cacheEx) { _logger.LogWarning(cacheEx, "DALCache read skipped — Dapr unavailable for key: {CacheKey}", dalKey); } var result = await factory(ct).ConfigureAwait(false); // Only the lock holder writes; null results are not cached (record not found). if (result is not null && acquired) { try { var json = JsonSerializer.Serialize(result); var meta = new Dictionary { ["ttlInSeconds"] = ttlSeconds.ToString() }; await _daprClient.SaveStateAsync(StateStore, dalKey, json, metadata: meta, cancellationToken: ct).ConfigureAwait(false); } catch (Exception cacheEx) { _logger.LogWarning(cacheEx, "DALCache write skipped — Dapr unavailable for key: {CacheKey}", dalKey); } } return result; } finally { if (acquired) sem.Release(); } } public async Task InvalidateAsync(string cacheKey, CancellationToken ct = default) { try { await _daprClient.DeleteStateAsync(StateStore, $"{KeyPrefix}{cacheKey}", cancellationToken: ct).ConfigureAwait(false); } catch (Exception cacheEx) { _logger.LogWarning(cacheEx, "DALCache invalidation skipped — Dapr unavailable for key: {CacheKey}", cacheKey); } } } }