Files
Finlytic/FinlyticCore/Services/Settings/SettingsService.cs
T

201 lines
6.9 KiB
C#

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<TContext> where TContext : DbContext
{
// --- 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);
}
public class SettingsService<TContext> : ISettingsService<TContext> where TContext : DbContext
{
private readonly Microsoft.Extensions.DependencyInjection.IServiceScopeFactory _scopeFactory;
private readonly ILogger<SettingsService<TContext>>? _logger;
// Fast In-Memory Cache: Key Schema: "KeyName"
private readonly ConcurrentDictionary<string, string> _cache = new();
public SettingsService(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory scopeFactory, ILogger<SettingsService<TContext>>? logger = null)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
#region SettingKey<T> Overloads
public Task<T> GetSettingAsync<T>(SettingKey<T> key, CancellationToken cancellationToken = default)
{
return GetSettingInternalAsync(key.Name, key.DefaultValue, cancellationToken);
}
public Task SetSettingAsync<T>(SettingKey<T> key, T value, CancellationToken cancellationToken = default)
{
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 Core Engine Logik
private async Task<T> GetSettingInternalAsync<T>(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<TContext>(scope.ServiceProvider);
// 2. Aus DB der spezifischen TContext-Instanz laden
var entity = await dbContext.Set<SettingEntity>()
.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<SettingEntity>().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<T>(string key, T value, CancellationToken cancellationToken)
{
var jsonValue = JsonSerializer.Serialize(value);
try
{
using var scope = _scopeFactory.CreateScope();
var dbContext = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<TContext>(scope.ServiceProvider);
var entity = await dbContext.Set<SettingEntity>()
.FirstOrDefaultAsync(s => s.Key == key, cancellationToken);
if (entity == null)
{
entity = new SettingEntity
{
Key = key,
ValueJson = jsonValue,
LastUpdatedUtc = DateTime.UtcNow
};
dbContext.Set<SettingEntity>().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<T>(string json, T defaultValue)
{
try
{
var result = JsonSerializer.Deserialize<T>(json);
return result ?? defaultValue;
}
catch
{
return defaultValue;
}
}
#endregion
}