using System.Data.Common;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace WorkClub.Infrastructure.Data.Interceptors;
///
/// Sets PostgreSQL RLS tenant context using SET LOCAL in explicit transactions.
/// For auto-commit reads: wraps in explicit transaction, applies SET LOCAL, commits on reader dispose.
/// For transactional writes: applies SET LOCAL once when transaction starts.
///
public class TenantDbTransactionInterceptor : DbCommandInterceptor, IDbTransactionInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
// Track transactions we created (so we know to commit/dispose them)
private readonly ConditionalWeakTable _ownedTxByCommand = new();
private readonly ConditionalWeakTable _ownedTxByReader = new();
public TenantDbTransactionInterceptor(
IHttpContextAccessor httpContextAccessor,
ILogger logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
// === READER COMMANDS (SELECT queries) ===
public override InterceptionResult ReaderExecuting(
DbCommand command, CommandEventData eventData, InterceptionResult result)
{
EnsureTransactionAndTenant(command);
return base.ReaderExecuting(command, eventData, result);
}
public override ValueTask> ReaderExecutingAsync(
DbCommand command, CommandEventData eventData, InterceptionResult result,
CancellationToken cancellationToken = default)
{
EnsureTransactionAndTenant(command);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
// After reader executes, transfer tx ownership from command to reader
public override DbDataReader ReaderExecuted(
DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
if (_ownedTxByCommand.TryGetValue(command, out var tx))
{
_ownedTxByCommand.Remove(command);
_ownedTxByReader.AddOrUpdate(result, tx);
}
return base.ReaderExecuted(command, eventData, result);
}
public override ValueTask ReaderExecutedAsync(
DbCommand command, CommandExecutedEventData eventData, DbDataReader result,
CancellationToken cancellationToken = default)
{
if (_ownedTxByCommand.TryGetValue(command, out var tx))
{
_ownedTxByCommand.Remove(command);
_ownedTxByReader.AddOrUpdate(result, tx);
}
return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
// When reader is disposed, commit and dispose the owned transaction
public override InterceptionResult DataReaderDisposing(
DbCommand command, DataReaderDisposingEventData eventData, InterceptionResult result)
{
if (_ownedTxByReader.TryGetValue(eventData.DataReader, out var tx))
{
_ownedTxByReader.Remove(eventData.DataReader);
try
{
tx.Commit();
_logger.LogDebug("Committed owned transaction for reader disposal");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to commit owned transaction on reader disposal");
try { tx.Rollback(); } catch { /* best-effort */ }
}
finally
{
tx.Dispose();
}
}
return base.DataReaderDisposing(command, eventData, result);
}
// === SCALAR COMMANDS ===
public override InterceptionResult