using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using StackExchange.Redis; namespace FrameworkDAL.CustomCode.HybridCache { /// /// Two-level hybrid cache: L1 = IMemoryCache (in-process), L2 = Redis (distributed). /// All Redis I/O uses async methods to avoid threadpool starvation. /// public class HybridCacheService { private readonly IMemoryCache _memoryCache; private readonly IDatabase? _redisCache; private readonly TimeSpan _defaultExpiration; private readonly bool _redisEnabled; // Shared options — constructing JsonSerializerOptions is expensive; reuse a single instance. private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true, WriteIndented = false }; public HybridCacheService( IMemoryCache memoryCache, IConnectionMultiplexer? redis, IConfiguration configuration) { _memoryCache = memoryCache; _defaultExpiration = TimeSpan.FromSeconds( configuration.GetValue("Redis:DefaultCacheDurationSeconds", 300)); if (redis != null && redis.IsConnected) { _redisCache = redis.GetDatabase(); _redisEnabled = true; } } // ── Async API (preferred) ───────────────────────────────────────────── /// /// Tries to retrieve a cached value. Checks L1 (memory) first, then L2 (Redis). /// On a Redis hit the value is backfilled into L1 for subsequent in-process reads. /// public async Task<(bool Found, T? Value)> TryGetValueAsync( string key, CancellationToken ct = default) { // L1 — memory cache (zero-cost, no I/O) if (_memoryCache.TryGetValue(key, out T? typed)) return (true, typed); // L2 — Redis if (_redisEnabled && _redisCache != null) { try { var redisValue = await _redisCache.StringGetAsync(key).ConfigureAwait(false); if (redisValue.HasValue) { var value = JsonSerializer.Deserialize((string)redisValue!, _jsonOptions); // Backfill L1 so subsequent in-process requests are cache-hot _memoryCache.Set(key, value, _defaultExpiration); return (true, value); } } catch (RedisConnectionException) { /* fail-silent */ } catch (RedisTimeoutException) { /* fail-silent */ } } return (false, default); } /// Sets a value in both L1 and L2 caches with an optional TTL. public async Task SetAsync( string key, T value, TimeSpan? expiration = null, CancellationToken ct = default) { var expiry = expiration ?? _defaultExpiration; _memoryCache.Set(key, value, expiry); if (_redisEnabled && _redisCache != null) { try { var serialized = JsonSerializer.Serialize(value, _jsonOptions); await _redisCache.StringSetAsync(key, serialized, expiry).ConfigureAwait(false); } catch (RedisConnectionException) { /* fail-silent */ } catch (RedisTimeoutException) { /* fail-silent */ } } } /// Removes a key from both L1 and L2 caches. public async Task RemoveAsync(string key, CancellationToken ct = default) { _memoryCache.Remove(key); if (_redisEnabled && _redisCache != null) { try { await _redisCache.KeyDeleteAsync(key).ConfigureAwait(false); } catch (RedisConnectionException) { /* fail-silent */ } catch (RedisTimeoutException) { /* fail-silent */ } } } // ── Sync API (obsolete — kept for backward compat; do not use in new code) ── /// [Obsolete("Use TryGetValueAsync instead. Synchronous Redis I/O blocks threadpool threads.")] public bool TryGetValue(string key, out T value) { // L1 only — Redis cannot be called synchronously without risk of deadlock if (_memoryCache.TryGetValue(key, out T? typed)) { value = typed!; return true; } value = default!; return false; } /// [Obsolete("Use SetAsync instead. Synchronous Redis I/O blocks threadpool threads.")] public void Set(string key, T value, TimeSpan? expiration = null) { _memoryCache.Set(key, value, expiration ?? _defaultExpiration); // Redis write omitted — callers must migrate to SetAsync for Redis persistence } /// [Obsolete("Use RemoveAsync instead. Synchronous Redis I/O blocks threadpool threads.")] public void Remove(string key) { _memoryCache.Remove(key); // Redis removal omitted — callers must migrate to RemoveAsync for full invalidation } } }