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
}
}
}