using Microsoft.Extensions.DependencyInjection; using Npgsql; using Tiku.Application.Tenancy; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Caching; using ZiggyCreatures.Caching.Fusion; namespace Tiku.Infrastructure.Tenancy; internal sealed record TenantDirectoryCacheEntry(bool Found, TenantDirectoryEntry? Entry); public sealed class TenantDirectory( NpgsqlDataSource dataSource, [FromKeyedServices(BusinessCachingServiceCollectionExtensions.CacheName)] IFusionCache cache) : ITenantDirectory { public Task FindByHostAsync( string host, CancellationToken cancellationToken = default) { const string sql = """ select t.id, t.slug, t.name, t.status, t.mode, d.host from tenant_domains d join tenants t on t.id = d.tenant_id where d.host = @lookup and d.status = 'active' and t.status = 'active' limit 1 """; return FindAsync(sql, "host", Normalize(host), cancellationToken); } public Task FindByCodeAsync( string tenantCode, CancellationToken cancellationToken = default) { const string sql = """ select t.id, t.slug, t.name, t.status, t.mode, null::text as host from tenants t where t.slug = @lookup and t.status = 'active' limit 1 """; return FindAsync(sql, "code", Normalize(tenantCode), cancellationToken); } private async Task FindAsync( string sql, string kind, string lookup, CancellationToken cancellationToken) { var cacheKey = CacheKey(kind, lookup); var cached = await cache.GetOrSetAsync( cacheKey, async (context, token) => { await using var command = dataSource.CreateCommand(sql); command.Parameters.AddWithValue("lookup", lookup); await using var reader = await command.ExecuteReaderAsync(token); if (!await reader.ReadAsync(token)) { context.Options.Duration = TimeSpan.FromSeconds(20); context.Options.MemoryCacheDuration = TimeSpan.FromSeconds(20); return new TenantDirectoryCacheEntry(false, null); } return new TenantDirectoryCacheEntry(true, new TenantDirectoryEntry( reader.GetGuid(0), reader.GetString(1), reader.GetString(2), ParseEnum(reader.GetString(3)), ParseEnum(reader.GetString(4)), reader.IsDBNull(5) ? null : reader.GetString(5))); }, options => { options.Duration = TimeSpan.FromSeconds(300); options.MemoryCacheDuration = TimeSpan.FromSeconds(30); }, token: cancellationToken); return cached.Entry; } private static TEnum ParseEnum(string value) where TEnum : struct, Enum { return Enum.Parse(value.Replace("_", string.Empty, StringComparison.Ordinal), true); } private static string Normalize(string value) { return value.Trim().ToLowerInvariant(); } internal static string CacheKey(string kind, string lookup) { return $"tenant-directory:v1:{kind}:{Normalize(lookup)}"; } }