using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; namespace NpgsqlRest; public interface IRoutineCache { bool Get(RoutineEndpoint endpoint, string key, out object? result); void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value); bool Remove(string key); } public static class CacheKeyHasher { /// /// Computes a SHA256 hash of the cache key and returns it as a hex string. /// The result is always 64 characters (256 bits / 4 bits per hex char). /// public static string ComputeHash(string key) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key)); return Convert.ToHexString(hash); } /// /// Returns the effective cache key based on the hashing configuration. /// If UseHashedCacheKeys is true and the key length exceeds HashKeyThreshold, returns a hashed key. /// Otherwise, returns the original key. /// public static string GetEffectiveKey(string key, CacheOptions options) { if (options.UseHashedCacheKeys && key.Length > options.HashKeyThreshold) { return ComputeHash(key); } return key; } } public class RoutineCache : IRoutineCache { private class CacheEntry { public object? Value { get; set; } public DateTime? ExpirationTime { get; set; } public bool IsExpired => ExpirationTime.HasValue && DateTime.UtcNow > ExpirationTime.Value; } private static readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private static Timer? _cleanupTimer; private static CacheOptions _options = new(); public static void Start(NpgsqlRestOptions options) { _options = options.CacheOptions; _cleanupTimer = new Timer( _ => CleanupExpiredEntriesInternal(), null, TimeSpan.FromSeconds(options.CacheOptions.MemoryCachePruneIntervalSeconds), TimeSpan.FromSeconds(options.CacheOptions.MemoryCachePruneIntervalSeconds)); } public static void Shutdown() { _cleanupTimer?.Dispose(); _cache.Clear(); } private static void CleanupExpiredEntriesInternal() { var expiredKeys = _cache .Where(kvp => kvp.Value.IsExpired) .Select(kvp => kvp.Key) .ToList(); foreach (var key in expiredKeys) { _cache.TryRemove(key, out _); } } public bool Get(RoutineEndpoint endpoint, string key, out object? result) { var effectiveKey = CacheKeyHasher.GetEffectiveKey(key, _options); if (_cache.TryGetValue(effectiveKey, out var entry)) { if (entry.IsExpired) { _cache.TryRemove(effectiveKey, out _); result = null; return false; } result = entry.Value; return true; } result = null; return false; } public void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value) { var effectiveKey = CacheKeyHasher.GetEffectiveKey(key, _options); var entry = new CacheEntry { Value = value, ExpirationTime = endpoint.CacheExpiresIn.HasValue ? DateTime.UtcNow + endpoint.CacheExpiresIn.Value : null }; _cache[effectiveKey] = entry; } public bool Remove(string key) { var effectiveKey = CacheKeyHasher.GetEffectiveKey(key, _options); return _cache.TryRemove(effectiveKey, out _); } }