using Dapper; using GB5Shared.Cache; using GB5Shared.Connection; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using PartnerDAL.DTO.ClientDomain; using PartnerDAL.Query.ClientDomain; namespace PartnerSL.Services { // Populates IMemoryCache with domain → tenant mappings at startup and every 5 minutes. // GetDomainTenant endpoint reads from this cache; falls back to DB on miss. // Queries GB5System directly via IApplicationConnection.Gb5SystemConnectionString() // (same pattern as ApiKeyAuthMiddleware) — no LoginDTO, no per-environment numeric // server/connection IDs to keep in sync. TCLIENTDOMAIN only ever lives in GB5System. public class DomainCacheService : IHostedService, IDisposable, IDomainCacheInvalidator { public static readonly string CachePrefix = "domain:"; private readonly IServiceScopeFactory _scopeFactory; private readonly IMemoryCache _cache; private readonly ILogger _logger; private Timer? _timer; public DomainCacheService( IServiceScopeFactory scopeFactory, IMemoryCache cache, ILogger logger) { _scopeFactory = scopeFactory; _cache = cache; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _ = RefreshAsync(cancellationToken); _timer = new Timer( _ => _ = RefreshAsync(CancellationToken.None), null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public async Task RefreshAsync(CancellationToken ct) { try { using var scope = _scopeFactory.CreateScope(); var appConnection = scope.ServiceProvider.GetRequiredService(); string connStr = await appConnection.Gb5SystemConnectionString().ConfigureAwait(false); using var conn = new SqlConnection(connStr); var domains = await conn.QueryAsync(ClientDomainQB.GET_ALL_ACTIVE_DOMAINS) .ConfigureAwait(false); var opts = new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(15) }; int count = 0; foreach (var d in domains) { if (string.IsNullOrWhiteSpace(d.DomainName)) continue; _cache.Set(CacheKey(d.DomainName), d, opts); count++; } _logger.LogInformation("DomainCacheService: loaded {Count} active domain mappings", count); } catch (Exception ex) { _logger.LogError(ex, "DomainCacheService: failed to refresh domain cache"); } } // Cache-aside: check cache first, fall back to DB on miss and populate cache. public async Task GetOrLoadAsync(string domainName, CancellationToken ct) { var key = CacheKey(domainName); if (_cache.TryGetValue(key, out ClientDomainDTO? dto) && dto != null) return dto; try { using var scope = _scopeFactory.CreateScope(); var appConnection = scope.ServiceProvider.GetRequiredService(); string connStr = await appConnection.Gb5SystemConnectionString().ConfigureAwait(false); using var conn = new SqlConnection(connStr); dto = await conn.QueryFirstOrDefaultAsync( ClientDomainQB.GET_DOMAIN_TENANT, new { DomainName = domainName.ToLowerInvariant() }).ConfigureAwait(false); if (dto != null) { _cache.Set(key, dto, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(15) }); _logger.LogInformation( "DomainCacheService: DB fallback resolved {DomainName} → ClientId={ClientId}", domainName, dto.ClientId); } else { _logger.LogWarning( "DomainCacheService: domain {DomainName} not found in TCLIENTDOMAIN", domainName); } return dto; } catch (Exception ex) { _logger.LogError(ex, "DomainCacheService: DB fallback failed for domain {DomainName}", domainName); return null; } } public void Invalidate(string domainName) => _cache.Remove(CacheKey(domainName)); public bool TryResolve(string domainName, out ClientDomainDTO? dto) => _cache.TryGetValue(CacheKey(domainName), out dto); public static string CacheKey(string domainName) => $"{CachePrefix}{domainName.ToLowerInvariant()}"; public void Dispose() => _timer?.Dispose(); } }