using System.Xml.Serialization; using ControlPlane.Core.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace ControlPlane.Core.Services; /// /// Reads and writes per-tenant XML config files under the ClientAssets folder. /// One file per tenant: {subdomain}.xml /// Thread-safe for concurrent reads; writes are serialized per subdomain via per-file locking. /// public class TenantRegistryService { private readonly string _folder; private readonly ILogger _logger; private static readonly XmlSerializer Serializer = new(typeof(TenantRecord)); // One lock object per subdomain so writes to different tenants never block each other private readonly System.Collections.Concurrent.ConcurrentDictionary _locks = new(StringComparer.OrdinalIgnoreCase); public TenantRegistryService(IConfiguration configuration, ILogger logger) { _folder = configuration["ClientAssets__Folder"] ?? configuration["ClientAssets:Folder"] ?? Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "ClientAssets")); _logger = logger; Directory.CreateDirectory(_folder); } // -- Write -- public void Save(TenantRecord record) { var path = FilePath(record.Subdomain); var gate = _locks.GetOrAdd(record.Subdomain, _ => new object()); lock (gate) { using var writer = new StreamWriter(path, append: false, System.Text.Encoding.UTF8); Serializer.Serialize(writer, record); } _logger.LogInformation("Saved tenant record: {Path}", path); } // -- Read -- public TenantRecord? TryGet(string subdomain) { var path = FilePath(subdomain); if (!File.Exists(path)) return null; using var reader = new StreamReader(path, System.Text.Encoding.UTF8); return (TenantRecord?)Serializer.Deserialize(reader); } public IReadOnlyList GetAll() { var results = new List(); foreach (var file in Directory.EnumerateFiles(_folder, "*.xml")) { try { using var reader = new StreamReader(file, System.Text.Encoding.UTF8); if (Serializer.Deserialize(reader) is TenantRecord record) results.Add(record); } catch (Exception ex) { _logger.LogWarning(ex, "Skipping malformed tenant file: {File}", file); } } return results; } public bool Exists(string subdomain) => File.Exists(FilePath(subdomain)); private string FilePath(string subdomain) => Path.Combine(_folder, $"{subdomain.ToLowerInvariant()}.xml"); }