namespace Tiku.Application.Security; public enum TenantResolutionSource { None, Host, TenantCode, Jwt, RefreshToken, Worker, System } public interface ITenantContext { Guid? TenantId { get; } string? TenantCode { get; } TenantResolutionSource ResolutionSource { get; } bool IsResolved { get; } bool IsSystem { get; } } public interface ITenantContextInitializer { void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source); void InitializeSystem(Guid? targetTenantId, string reason); } public sealed class TenantContext : ITenantContext, ITenantContextInitializer { public Guid? TenantId { get; private set; } public string? TenantCode { get; private set; } public TenantResolutionSource ResolutionSource { get; private set; } public bool IsResolved => TenantId.HasValue; public bool IsSystem { get; private set; } internal string? SystemReason { get; private set; } public void Initialize(Guid tenantId, string? tenantCode, TenantResolutionSource source) { if (tenantId == Guid.Empty) { throw new ArgumentException("Tenant ID cannot be empty.", nameof(tenantId)); } if (IsSystem) { throw new InvalidOperationException("A system tenant context cannot be replaced."); } if (TenantId.HasValue && TenantId.Value != tenantId) { throw new TenantContextConflictException(TenantId.Value, tenantId); } var wasResolved = TenantId.HasValue; TenantId = tenantId; TenantCode = string.IsNullOrWhiteSpace(tenantCode) ? TenantCode : tenantCode.Trim(); if (!wasResolved) { ResolutionSource = source; } } public void InitializeSystem(Guid? targetTenantId, string reason) { if (IsResolved || IsSystem) { throw new InvalidOperationException("Tenant context has already been initialized."); } if (string.IsNullOrWhiteSpace(reason)) { throw new ArgumentException("A system scope requires an audit reason.", nameof(reason)); } TenantId = targetTenantId; ResolutionSource = TenantResolutionSource.System; IsSystem = true; SystemReason = reason.Trim(); } } public sealed class TenantContextConflictException(Guid expectedTenantId, Guid actualTenantId) : Exception($"Resolved tenant '{expectedTenantId}' conflicts with authenticated tenant '{actualTenantId}'.") { public Guid ExpectedTenantId { get; } = expectedTenantId; public Guid ActualTenantId { get; } = actualTenantId; }