Seeding reorg
This commit is contained in:
74
SVSim.EmulatedEntrypoint/Services/GameConfigService.cs
Normal file
74
SVSim.EmulatedEntrypoint/Services/GameConfigService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models.Config;
|
||||
using SVSim.Database.Services;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Three-tier resolver for <see cref="IGameConfigService"/>: GameConfigs row → IConfiguration
|
||||
/// section under "GameConfig:<name>" → <c>T.ShippedDefaults()</c> (via reflection) → <c>new T()</c>.
|
||||
/// <para>
|
||||
/// Atomic per section: the first tier that has the section wins entirely; no per-property merging.
|
||||
/// Scoped lifetime — one DB read per request — matches today's <c>GameConfigRoot</c> behavior.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class GameConfigService : IGameConfigService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, SectionMetadata> _metaCache = new();
|
||||
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IConfiguration _appSettings;
|
||||
|
||||
public GameConfigService(SVSimDbContext db, IConfiguration appSettings)
|
||||
{
|
||||
_db = db;
|
||||
_appSettings = appSettings;
|
||||
}
|
||||
|
||||
public T Get<T>() where T : class, new()
|
||||
{
|
||||
var meta = GetMeta(typeof(T));
|
||||
|
||||
// Tier 1: DB row
|
||||
var row = _db.GameConfigs.AsNoTracking().FirstOrDefault(s => s.SectionName == meta.SectionName);
|
||||
if (row is not null)
|
||||
{
|
||||
return (T?)JsonSerializer.Deserialize(row.ValueJson, typeof(T))
|
||||
?? throw new InvalidOperationException(
|
||||
$"GameConfigs row '{meta.SectionName}' deserialised to null — corrupt jsonb?");
|
||||
}
|
||||
|
||||
// Tier 2: appsettings.json under "GameConfig:<name>"
|
||||
var configSection = _appSettings.GetSection($"GameConfig:{meta.SectionName}");
|
||||
if (configSection.Exists())
|
||||
{
|
||||
var fromAppsettings = configSection.Get<T>();
|
||||
if (fromAppsettings is not null) return fromAppsettings;
|
||||
}
|
||||
|
||||
// Tier 3: ShippedDefaults() if present, else parameterless ctor
|
||||
if (meta.ShippedDefaultsFactory is not null)
|
||||
{
|
||||
return (T)meta.ShippedDefaultsFactory.Invoke(null, null)!;
|
||||
}
|
||||
return new T();
|
||||
}
|
||||
|
||||
private static SectionMetadata GetMeta(Type t) => _metaCache.GetOrAdd(t, static type =>
|
||||
{
|
||||
var attr = type.GetCustomAttribute<ConfigSectionAttribute>(inherit: false)
|
||||
?? throw new InvalidOperationException(
|
||||
$"{type.FullName} is not marked with [ConfigSection(...)] — IGameConfigService can't resolve it.");
|
||||
var factory = type.GetMethod("ShippedDefaults",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
binder: null, types: Type.EmptyTypes, modifiers: null);
|
||||
return new SectionMetadata(attr.Name, factory);
|
||||
});
|
||||
|
||||
private sealed record SectionMetadata(string SectionName, MethodInfo? ShippedDefaultsFactory);
|
||||
}
|
||||
Reference in New Issue
Block a user