Files

556 lines
20 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FinlyticCore.Database;
using FinlyticCore.Dtos.Settings;
using FinlyticCore.Entities.Settings;
using FinlyticCore.Models.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FinlyticCore.Services;
public interface ISettingsService
{
// --- 1. Typsicherer Zugriff über SettingKey<T> (Empfohlen) ---
Task<T> GetSettingAsync<T>(SettingKey<T> key, CancellationToken cancellationToken = default);
Task SetSettingAsync<T>(SettingKey<T> key, T value, CancellationToken cancellationToken = default);
// --- 2. Dynamischer Zugriff über Enum-Key ---
Task<T> GetSettingAsync<TEnum, T>(TEnum enumKey, T defaultValue = default!, CancellationToken cancellationToken = default) where TEnum : struct, Enum;
Task SetSettingAsync<TEnum, T>(TEnum enumKey, T value, CancellationToken cancellationToken = default) where TEnum : struct, Enum;
// --- 3. Dynamischer Zugriff über String-Key ---
Task<T> GetSettingAsync<T>(string key, T defaultValue = default!, CancellationToken cancellationToken = default);
Task SetSettingAsync<T>(string key, T value, CancellationToken cancellationToken = default);
// --- 4. Reflection-Erkennung & Bulk-Verwaltung für Web UI / MQTT ---
Task<List<DynamicSettingDto>> GetAllRegisteredSettingsAsync(IEnumerable<Type>? customKeyHolders = null, CancellationToken cancellationToken = default);
Task UpdateSettingsAsync(Dictionary<string, object?> updatedSettings, CancellationToken cancellationToken = default);
}
public class SettingsService : ISettingsService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<SettingsService>? _logger;
// Fast In-Memory Cache: Key Schema: "KeyName" -> JSON string
private readonly ConcurrentDictionary<string, string> _cache = new(StringComparer.OrdinalIgnoreCase);
public SettingsService(
IServiceScopeFactory scopeFactory,
ILogger<SettingsService>? logger = null)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_logger = logger;
}
#region SettingKey<T> Overloads
public Task<T> GetSettingAsync<T>(SettingKey<T> key, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(key);
return GetSettingInternalAsync(key.Name, key.DefaultValue, cancellationToken);
}
public Task SetSettingAsync<T>(SettingKey<T> key, T value, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(key);
return SetSettingInternalAsync(key.Name, value, cancellationToken);
}
#endregion
#region Enum-Key Overloads
public Task<T> GetSettingAsync<TEnum, T>(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, T>(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<T> GetSettingAsync<T>(string key, T defaultValue = default!,
CancellationToken cancellationToken = default)
{
return GetSettingInternalAsync(key, defaultValue, cancellationToken);
}
public Task SetSettingAsync<T>(string key, T value, CancellationToken cancellationToken = default)
{
return SetSettingInternalAsync(key, value, cancellationToken);
}
#endregion
#region Bulk & Reflection Discovery
public async Task<List<DynamicSettingDto>> GetAllRegisteredSettingsAsync(
IEnumerable<Type>? customKeyHolders = null,
CancellationToken cancellationToken = default)
{
var isCustomScoped = customKeyHolders != null && customKeyHolders.Any();
var holderTypes = isCustomScoped
? customKeyHolders!.ToList()
: new List<Type> { typeof(CoreSettingKeys) };
var resultList = new List<DynamicSettingDto>();
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// 1. Reflection auf allen SettingKey<T> Feldern
foreach (var type in holderTypes.Distinct())
{
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
foreach (var field in fields)
{
var fieldType = field.FieldType;
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(SettingKey<>))
{
var valType = fieldType.GetGenericArguments()[0];
var settingKeyObj = field.GetValue(null);
if (settingKeyObj == null) continue;
var nameProp = fieldType.GetProperty("Name");
var defaultProp = fieldType.GetProperty("DefaultValue");
var keyName = nameProp?.GetValue(settingKeyObj)?.ToString() ?? field.Name;
if (seenKeys.Contains(keyName)) continue;
seenKeys.Add(keyName);
var defVal = defaultProp?.GetValue(settingKeyObj);
var typeName = MapToSimpleTypeName(valType);
// Aktuellen Wert aus DB / Cache lesen
var currentRawValue = await GetSettingInternalObjectAsync(keyName, valType, defVal, cancellationToken);
resultList.Add(new DynamicSettingDto(
Key: keyName,
Value: currentRawValue,
Type: typeName,
Description: FormatDescriptionFromKey(keyName),
UpdatedAt: DateTime.UtcNow
));
}
}
}
// 2. Prüfen, ob in der DB weitere gespeicherte Settings existieren (nur wenn nicht strikt auf custom KeyHolders begrenzt)
if (!isCustomScoped)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
if (dbContext != null)
{
var dbSettings = await dbContext.DynamicSettings.AsNoTracking().ToListAsync(cancellationToken);
foreach (var dbSetting in dbSettings)
{
if (!seenKeys.Contains(dbSetting.Key))
{
seenKeys.Add(dbSetting.Key);
var (inferredVal, inferredType) = InferJsonValueAndType(dbSetting.ValueJson);
resultList.Add(new DynamicSettingDto(
Key: dbSetting.Key,
Value: inferredVal,
Type: inferredType,
Description: FormatDescriptionFromKey(dbSetting.Key),
UpdatedAt: dbSetting.LastUpdatedUtc
));
}
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "[SettingsService] Error reading database settings during GetAllRegisteredSettingsAsync.");
}
}
return resultList.OrderBy(s => s.Key).ToList();
}
public async Task UpdateSettingsAsync(Dictionary<string, object?> updatedSettings, CancellationToken cancellationToken = default)
{
if (updatedSettings == null || updatedSettings.Count == 0) return;
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
foreach (var (key, rawValue) in updatedSettings)
{
if (string.IsNullOrWhiteSpace(key)) continue;
string jsonValue = NormalizeJsonValue(rawValue);
_cache[key] = jsonValue;
if (dbContext != null)
{
var entity = await dbContext.DynamicSettings.FirstOrDefaultAsync(s => s.Key == key, cancellationToken);
if (entity == null)
{
entity = new SettingEntity
{
Key = key,
ValueJson = jsonValue,
LastUpdatedUtc = DateTime.UtcNow
};
dbContext.DynamicSettings.Add(entity);
}
else
{
entity.ValueJson = jsonValue;
entity.LastUpdatedUtc = DateTime.UtcNow;
}
}
}
if (dbContext != null)
{
await dbContext.SaveChangesAsync(cancellationToken);
}
}
#endregion
#region Internal Engine Logic
private async Task<T> GetSettingInternalAsync<T>(string key, T defaultValue, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(key, out var cachedJson))
{
return DeserializeValue(cachedJson, defaultValue);
}
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
if (dbContext == null)
{
var defaultJson = NormalizeJsonValue(defaultValue);
_cache[key] = defaultJson;
return defaultValue;
}
var entity = await dbContext.DynamicSettings
.FirstOrDefaultAsync(s => s.Key == key, cancellationToken);
if (entity == null)
{
var defaultJson = NormalizeJsonValue(defaultValue);
entity = new SettingEntity
{
Key = key,
ValueJson = defaultJson,
LastUpdatedUtc = DateTime.UtcNow
};
dbContext.DynamicSettings.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
_cache[key] = defaultJson;
return defaultValue;
}
_cache[key] = entity.ValueJson;
return DeserializeValue(entity.ValueJson, defaultValue);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "[SettingsService] Setting '{Key}' could not be loaded or initialized in DB. Using default value.", key);
_cache[key] = NormalizeJsonValue(defaultValue);
return defaultValue;
}
}
private async Task<object?> GetSettingInternalObjectAsync(string key, Type valueType, object? defaultValue, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(key, out var cachedJson))
{
return DeserializeObject(cachedJson, valueType, defaultValue);
}
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
if (dbContext == null) return defaultValue;
var entity = await dbContext.DynamicSettings.FirstOrDefaultAsync(s => s.Key == key, cancellationToken);
if (entity == null)
{
var defaultJson = NormalizeJsonValue(defaultValue);
entity = new SettingEntity
{
Key = key,
ValueJson = defaultJson,
LastUpdatedUtc = DateTime.UtcNow
};
dbContext.DynamicSettings.Add(entity);
await dbContext.SaveChangesAsync(cancellationToken);
_cache[key] = defaultJson;
return defaultValue;
}
_cache[key] = entity.ValueJson;
return DeserializeObject(entity.ValueJson, valueType, defaultValue);
}
catch
{
return defaultValue;
}
}
private async Task SetSettingInternalAsync<T>(string key, T value, CancellationToken cancellationToken)
{
var jsonValue = NormalizeJsonValue(value);
_cache[key] = jsonValue;
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetService<ISettingsDbContext>();
if (dbContext == null) return;
var entity = await dbContext.DynamicSettings
.FirstOrDefaultAsync(s => s.Key == key, cancellationToken);
if (entity == null)
{
entity = new SettingEntity
{
Key = key,
ValueJson = jsonValue,
LastUpdatedUtc = DateTime.UtcNow
};
dbContext.DynamicSettings.Add(entity);
}
else
{
entity.ValueJson = jsonValue;
entity.LastUpdatedUtc = DateTime.UtcNow;
}
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "[SettingsService] Setting '{Key}' could not be saved to DB.", key);
}
}
private static string NormalizeJsonValue(object? rawValue)
{
if (rawValue == null) return "null";
if (rawValue is JsonElement jsonElem)
{
if (jsonElem.ValueKind == JsonValueKind.String)
{
var str = jsonElem.GetString()?.Trim() ?? string.Empty;
var unquoted = str.Trim('\"', ' ');
if (bool.TryParse(unquoted, out var b)) return b ? "true" : "false";
if (long.TryParse(unquoted, out var l)) return l.ToString();
if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d.ToString(CultureInfo.InvariantCulture);
return JsonSerializer.Serialize(str);
}
return jsonElem.GetRawText();
}
if (rawValue is string s)
{
var unquoted = s.Trim('\"', ' ');
if (bool.TryParse(unquoted, out var b)) return b ? "true" : "false";
if (long.TryParse(unquoted, out var l)) return l.ToString();
if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d.ToString(CultureInfo.InvariantCulture);
return JsonSerializer.Serialize(s);
}
if (rawValue is bool bVal) return bVal ? "true" : "false";
if (rawValue is int or long or short or byte) return rawValue.ToString()!;
if (rawValue is double or float or decimal) return Convert.ToString(rawValue, CultureInfo.InvariantCulture)!;
return JsonSerializer.Serialize(rawValue);
}
private static T DeserializeValue<T>(string json, T defaultValue)
{
if (string.IsNullOrWhiteSpace(json)) return defaultValue;
try
{
var unquoted = json.Trim('\"', ' ');
if (typeof(T) == typeof(bool))
{
if (bool.TryParse(unquoted, out var b))
{
return (T)(object)b;
}
}
else if (typeof(T) == typeof(int))
{
if (int.TryParse(unquoted, out var i))
{
return (T)(object)i;
}
}
else if (typeof(T) == typeof(long))
{
if (long.TryParse(unquoted, out var l))
{
return (T)(object)l;
}
}
else if (typeof(T) == typeof(double))
{
if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
{
return (T)(object)d;
}
}
else if (typeof(T) == typeof(string))
{
var trimmed = json.Trim();
if (trimmed.StartsWith("\"") && trimmed.EndsWith("\"") && trimmed.Length >= 2)
{
try { return (T)(object)(JsonSerializer.Deserialize<string>(trimmed) ?? trimmed.Trim('\"')); }
catch { return (T)(object)trimmed.Trim('\"'); }
}
return (T)(object)trimmed;
}
var result = JsonSerializer.Deserialize<T>(json);
return result ?? defaultValue;
}
catch
{
return defaultValue;
}
}
private static object? DeserializeObject(string json, Type valueType, object? defaultValue)
{
if (string.IsNullOrWhiteSpace(json)) return defaultValue;
try
{
var unquoted = json.Trim('\"', ' ');
if (valueType == typeof(bool))
{
if (bool.TryParse(unquoted, out var b)) return b;
}
else if (valueType == typeof(int))
{
if (int.TryParse(unquoted, out var i)) return i;
}
else if (valueType == typeof(long))
{
if (long.TryParse(unquoted, out var l)) return l;
}
else if (valueType == typeof(double))
{
if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d;
}
else if (valueType == typeof(string))
{
var trimmed = json.Trim();
if (trimmed.StartsWith("\"") && trimmed.EndsWith("\"") && trimmed.Length >= 2)
{
try { return JsonSerializer.Deserialize<string>(trimmed) ?? trimmed.Trim('\"'); }
catch { return trimmed.Trim('\"'); }
}
return trimmed;
}
return JsonSerializer.Deserialize(json, valueType) ?? defaultValue;
}
catch
{
return defaultValue;
}
}
private static string MapToSimpleTypeName(Type type)
{
if (type == typeof(bool)) return "bool";
if (type == typeof(int) || type == typeof(short) || type == typeof(byte) || type == typeof(long)) return "int";
if (type == typeof(double) || type == typeof(float) || type == typeof(decimal)) return "double";
return "string";
}
private static (object? Value, string Type) InferJsonValueAndType(string json)
{
if (string.IsNullOrWhiteSpace(json)) return (string.Empty, "string");
try
{
var unquoted = json.Trim('\"', ' ');
if (bool.TryParse(unquoted, out var b)) return (b, "bool");
if (long.TryParse(unquoted, out var l)) return (l, "int");
if (double.TryParse(unquoted, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return (d, "double");
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
return root.ValueKind switch
{
JsonValueKind.True => (true, "bool"),
JsonValueKind.False => (false, "bool"),
JsonValueKind.Number when root.TryGetInt64(out var i) => (i, "int"),
JsonValueKind.Number => (root.GetDouble(), "double"),
JsonValueKind.String => (root.GetString(), "string"),
_ => (json, "string")
};
}
catch
{
return (json, "string");
}
}
private static string FormatDescriptionFromKey(string key)
{
if (key.StartsWith("Logging.Channel.", StringComparison.OrdinalIgnoreCase))
{
return $"Logging-Kanal für {key.Substring(16)} (Ein/Aus)";
}
if (key.StartsWith("Feature.", StringComparison.OrdinalIgnoreCase))
{
return $"Feature-Toggle für {key.Substring(8)}";
}
if (key.StartsWith("Cache.", StringComparison.OrdinalIgnoreCase))
{
return $"Cache-Konfiguration ({key.Substring(6)})";
}
if (key.StartsWith("Scraper.", StringComparison.OrdinalIgnoreCase))
{
return $"Scraper-Konfiguration ({key.Substring(8)})";
}
return key;
}
#endregion
}