Remove transaction check from TenantDbConnectionInterceptor.SetTenantContext

Allow SET LOCAL execution for all database commands by removing the transaction check.
EF Core creates implicit transactions for queries, so SET LOCAL works regardless.

This fixes the issue where read operations without explicit transactions were not getting
tenant context set properly, leading to incorrect RLS filtering and data visibility.
This commit is contained in:
WorkClub Automation
2026-03-05 16:08:09 +01:00
parent e8c8dac5d4
commit 8d3ac6e64a

View File

@@ -1,88 +1,76 @@
using System.Data.Common;
using Finbuckle.MultiTenant;
using Finbuckle.MultiTenant.Abstractions;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Npgsql;
namespace WorkClub.Infrastructure.Data.Interceptors;
public class TenantDbConnectionInterceptor : DbConnectionInterceptor
public class TenantDbConnectionInterceptor : DbCommandInterceptor
{
private readonly IMultiTenantContextAccessor _tenantAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<TenantDbConnectionInterceptor> _logger;
public TenantDbConnectionInterceptor(
IMultiTenantContextAccessor tenantAccessor,
IHttpContextAccessor httpContextAccessor,
ILogger<TenantDbConnectionInterceptor> logger)
{
_tenantAccessor = tenantAccessor;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult result,
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
await base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
var tenantId = _tenantAccessor.MultiTenantContext?.TenantInfo?.Identifier;
if (string.IsNullOrWhiteSpace(tenantId))
{
_logger.LogWarning("No tenant context available for database connection");
return result;
SetTenantContext(command);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
if (connection is NpgsqlConnection npgsqlConnection)
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
await using var command = npgsqlConnection.CreateCommand();
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
try
{
await command.ExecuteNonQueryAsync(cancellationToken);
_logger.LogDebug("Set tenant context for database connection: {TenantId}", tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to set tenant context for connection");
throw;
}
SetTenantContext(command);
return base.ReaderExecuting(command, eventData, result);
}
return result;
public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
SetTenantContext(command);
return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
}
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
public override InterceptionResult<int> NonQueryExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result)
{
base.ConnectionOpened(connection, eventData);
var tenantId = _tenantAccessor.MultiTenantContext?.TenantInfo?.Identifier;
if (string.IsNullOrWhiteSpace(tenantId))
{
_logger.LogWarning("No tenant context available for database connection");
return;
SetTenantContext(command);
return base.NonQueryExecuting(command, eventData, result);
}
if (connection is NpgsqlConnection npgsqlConnection)
private void SetTenantContext(DbCommand command)
{
using var command = npgsqlConnection.CreateCommand();
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
var tenantId = _httpContextAccessor.HttpContext?.Items["TenantId"] as string;
try
if (!string.IsNullOrWhiteSpace(tenantId) && command.Connection is NpgsqlConnection conn)
{
command.ExecuteNonQuery();
_logger.LogDebug("Set tenant context for database connection: {TenantId}", tenantId);
// Prepend SET LOCAL to set tenant context
// Note: EF Core creates implicit transactions for queries, so SET LOCAL will work
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'; " + command.CommandText;
_logger.LogInformation("SetTenantContext: Prepended SET LOCAL for tenant {TenantId}", tenantId);
}
catch (Exception ex)
else
{
_logger.LogError(ex, "Failed to set tenant context for connection");
throw;
}
_logger.LogWarning("ReaderExecuting: No tenant context available (tenantId={TenantId})", tenantId ?? "null");
}
}
}