using System.Data; using System.Data.Common; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Npgsql; namespace WorkClub.Infrastructure.Data.Interceptors; public class TenantDbConnectionInterceptor : DbCommandInterceptor { private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public TenantDbConnectionInterceptor( IHttpContextAccessor httpContextAccessor, ILogger logger) { _httpContextAccessor = httpContextAccessor; _logger = logger; } public override ValueTask> ReaderExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { SetTenantContext(command); return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } public override InterceptionResult ReaderExecuting( DbCommand command, CommandEventData eventData, InterceptionResult result) { SetTenantContext(command); return base.ReaderExecuting(command, eventData, result); } public override ValueTask> NonQueryExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { SetTenantContext(command); return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); } public override InterceptionResult NonQueryExecuting( DbCommand command, CommandEventData eventData, InterceptionResult result) { SetTenantContext(command); return base.NonQueryExecuting(command, eventData, result); } private void SetTenantContext(DbCommand command) { var tenantId = _httpContextAccessor.HttpContext?.Items["TenantId"] as string; if (!string.IsNullOrWhiteSpace(tenantId) && command.Connection is NpgsqlConnection conn) { try { // If no transaction exists AND connection is open, start one // SET LOCAL only persists within a transaction, so we need explicit transaction scope if (command.Transaction == null && conn.State == ConnectionState.Open) { command.Transaction = (NpgsqlTransaction)conn.BeginTransaction(); _logger.LogInformation("SetTenantContext: Started transaction for tenant context"); } // Execute SET LOCAL as a separate command on the same connection // This ensures the session variable is properly set before the main query executes using var setLocalCmd = conn.CreateCommand(); setLocalCmd.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'"; // Use the same transaction if one exists (which it should now) if (command.Transaction is NpgsqlTransaction transaction) { setLocalCmd.Transaction = transaction; } // Execute synchronously (safe in interceptor context) setLocalCmd.ExecuteNonQuery(); _logger.LogInformation("SetTenantContext: Executed SET LOCAL for tenant {TenantId}", tenantId); } catch (Exception ex) { _logger.LogError(ex, "SetTenantContext: Failed to execute SET LOCAL for tenant {TenantId}", tenantId); throw; } } else { _logger.LogWarning("SetTenantContext: No tenant context available (tenantId={TenantId})", tenantId ?? "null"); } } }