using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore; using Tiku.Application.Security; using System.Diagnostics; using System.Text.Json; using Tiku.Domain.Operations; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Tenancy; public sealed class TenantExecutionScope( IServiceScopeFactory scopeFactory, ILogger logger) : ITenantExecutionScope { public Task ExecuteAsync( SystemScopeRequest request, Func operation, CancellationToken cancellationToken = default) => ExecuteAsync(request, async (provider, token) => { await operation(provider, token); return null; }, cancellationToken); public async Task ExecuteAsync( SystemScopeRequest request, Func> operation, CancellationToken cancellationToken = default) { Validate(request, operation); await using var scope = scopeFactory.CreateAsyncScope(); var initializer = scope.ServiceProvider.GetRequiredService(); initializer.InitializeSystem(request.TargetTenantId, request.Reason); logger.LogWarning( "Entering audited system scope. CallerType={CallerType} Caller={Caller} TargetTenantId={TargetTenantId} CorrelationId={CorrelationId} Reason={Reason}", request.CallerType, request.Caller, request.TargetTenantId, request.CorrelationId, request.Reason); var dbContext = scope.ServiceProvider.GetRequiredService(); var started = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); await using var transaction = dbContext.Database.IsRelational() ? await dbContext.Database.BeginTransactionAsync(cancellationToken) : null; await WriteAuditAsync(dbContext, request, "system_scope.entered", started, null, null, cancellationToken); try { var result = await operation(scope.ServiceProvider, cancellationToken); await WriteAuditAsync( dbContext, request, "system_scope.completed", started, stopwatch.ElapsedMilliseconds, null, cancellationToken); if (transaction is not null) { await transaction.CommitAsync(cancellationToken); } return result; } catch (Exception exception) { if (transaction is not null) { await transaction.RollbackAsync(CancellationToken.None); dbContext.ChangeTracker.Clear(); await WriteAuditAsync( dbContext, request, "system_scope.entered", started, null, null, CancellationToken.None); } await WriteAuditAsync( dbContext, request, "system_scope.failed", started, stopwatch.ElapsedMilliseconds, exception.GetType().Name, CancellationToken.None); throw; } } private static void Validate( SystemScopeRequest request, Func> operation) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(operation); ArgumentException.ThrowIfNullOrWhiteSpace(request.Caller); ArgumentException.ThrowIfNullOrWhiteSpace(request.Reason); ArgumentException.ThrowIfNullOrWhiteSpace(request.CorrelationId); if (request.TargetTenantId is null && !request.IsGlobal) { throw new ArgumentException("System scope requires a target tenant or an explicit global declaration.", nameof(request)); } if (request.TargetTenantId is not null && request.IsGlobal) { throw new ArgumentException("A tenant-targeted system scope cannot also be global.", nameof(request)); } if (request.IsGlobal && request.CallerType is SystemScopeCallerType.Worker or SystemScopeCallerType.PublicQuestionBank) { throw new ArgumentException("Worker and public question bank scopes must target a tenant.", nameof(request)); } if (!Enum.IsDefined(request.CallerType)) { throw new ArgumentOutOfRangeException(nameof(request), "Unknown system scope caller type."); } } private static async Task WriteAuditAsync( TikuDbContext dbContext, SystemScopeRequest request, string action, DateTimeOffset startedAt, long? elapsedMilliseconds, string? failureType, CancellationToken cancellationToken) { dbContext.AuditLogs.Add(new AuditLog { TenantId = request.TargetTenantId, Action = action, TargetType = "system_scope", TargetId = request.CorrelationId, Details = JsonSerializer.SerializeToElement(new { callerType = request.CallerType.ToString(), request.Caller, request.Reason, request.CorrelationId, request.IsGlobal, startedAt, elapsedMilliseconds, failureType }) }); await dbContext.SaveChangesAsync(cancellationToken); } }