using System; using System.Collections.Concurrent; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Entities.Settings; using FinlyticCore.Models.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace FinlyticCore.Services; public interface ISettingsService where TContext : DbContext { // --- 1. Typsicherer Zugriff über SettingKey (Empfohlen) --- Task GetSettingAsync(SettingKey key, CancellationToken cancellationToken = default); Task SetSettingAsync(SettingKey key, T value, CancellationToken cancellationToken = default); // --- 2. Dynamischer Zugriff über Enum-Key --- Task GetSettingAsync(TEnum enumKey, T defaultValue = default!, CancellationToken cancellationToken = default) where TEnum : struct, Enum; Task SetSettingAsync(TEnum enumKey, T value, CancellationToken cancellationToken = default) where TEnum : struct, Enum; // --- 3. Dynamischer Zugriff über String-Key --- Task GetSettingAsync(string key, T defaultValue = default!, CancellationToken cancellationToken = default); Task SetSettingAsync(string key, T value, CancellationToken cancellationToken = default); } public class SettingsService : ISettingsService where TContext : DbContext { private readonly Microsoft.Extensions.DependencyInjection.IServiceScopeFactory _scopeFactory; private readonly ILogger>? _logger; // Fast In-Memory Cache: Key Schema: "KeyName" private readonly ConcurrentDictionary _cache = new(); public SettingsService(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory scopeFactory, ILogger>? logger = null) { _scopeFactory = scopeFactory; _logger = logger; } #region SettingKey Overloads public Task GetSettingAsync(SettingKey key, CancellationToken cancellationToken = default) { return GetSettingInternalAsync(key.Name, key.DefaultValue, cancellationToken); } public Task SetSettingAsync(SettingKey key, T value, CancellationToken cancellationToken = default) { return SetSettingInternalAsync(key.Name, value, cancellationToken); } #endregion #region Enum-Key Overloads public Task GetSettingAsync(TEnum enumKey, T defaultValue = default!, CancellationToken cancellationToken = default) where TEnum : struct, Enum { var keyName = $"{typeof(TEnum).Name}.{enumKey}"; return GetSettingInternalAsync(keyName, defaultValue, cancellationToken); } public Task SetSettingAsync(TEnum enumKey, T value, CancellationToken cancellationToken = default) where TEnum : struct, Enum { var keyName = $"{typeof(TEnum).Name}.{enumKey}"; return SetSettingInternalAsync(keyName, value, cancellationToken); } #endregion #region String-Key Overloads public Task GetSettingAsync(string key, T defaultValue = default!, CancellationToken cancellationToken = default) { return GetSettingInternalAsync(key, defaultValue, cancellationToken); } public Task SetSettingAsync(string key, T value, CancellationToken cancellationToken = default) { return SetSettingInternalAsync(key, value, cancellationToken); } #endregion #region Core Engine Logik private async Task GetSettingInternalAsync(string key, T defaultValue, CancellationToken cancellationToken) { // 1. Zuerst im In-Memory Cache prüfen if (_cache.TryGetValue(key, out var cachedJson)) { return DeserializeValue(cachedJson, defaultValue); } try { using var scope = _scopeFactory.CreateScope(); var dbContext = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider); // 2. Aus DB der spezifischen TContext-Instanz laden var entity = await dbContext.Set() .FirstOrDefaultAsync(s => s.Key == key, cancellationToken); // 3. Falls noch nicht vorhanden: In DB anlegen (Seed on Demand) if (entity == null) { var defaultJson = JsonSerializer.Serialize(defaultValue); entity = new SettingEntity { Key = key, ValueJson = defaultJson, LastUpdatedUtc = DateTime.UtcNow }; dbContext.Set().Add(entity); await dbContext.SaveChangesAsync(cancellationToken); _cache[key] = defaultJson; return defaultValue; } // In Cache legen & Wert zurückgeben _cache[key] = entity.ValueJson; return DeserializeValue(entity.ValueJson, defaultValue); } catch (Exception ex) { _logger?.LogWarning(ex, "Setting '{Key}' could not be loaded or initialized in DB. Using default value in memory.", key); _cache[key] = JsonSerializer.Serialize(defaultValue); return defaultValue; } } private async Task SetSettingInternalAsync(string key, T value, CancellationToken cancellationToken) { var jsonValue = JsonSerializer.Serialize(value); try { using var scope = _scopeFactory.CreateScope(); var dbContext = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider); var entity = await dbContext.Set() .FirstOrDefaultAsync(s => s.Key == key, cancellationToken); if (entity == null) { entity = new SettingEntity { Key = key, ValueJson = jsonValue, LastUpdatedUtc = DateTime.UtcNow }; dbContext.Set().Add(entity); } else { entity.ValueJson = jsonValue; entity.LastUpdatedUtc = DateTime.UtcNow; } await dbContext.SaveChangesAsync(cancellationToken); } catch (Exception ex) { _logger?.LogWarning(ex, "Setting '{Key}' could not be saved to DB.", key); } // Cache trotzdem aktualisieren _cache[key] = jsonValue; } private static T DeserializeValue(string json, T defaultValue) { try { var result = JsonSerializer.Deserialize(json); return result ?? defaultValue; } catch { return defaultValue; } } #endregion }