OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using System.Xml.Serialization;
|
||||
using ControlPlane.Core.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ControlPlane.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TenantRegistryService
|
||||
{
|
||||
private readonly string _folder;
|
||||
private readonly ILogger<TenantRegistryService> _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<string, object> _locks = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public TenantRegistryService(IConfiguration configuration, ILogger<TenantRegistryService> 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<TenantRecord> GetAll()
|
||||
{
|
||||
var results = new List<TenantRecord>();
|
||||
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");
|
||||
}
|
||||
Reference in New Issue
Block a user