fix: resolve ObjectDisposedException in ClubService.GetMyClubsAsync()

Create fresh NpgsqlConnection per tenant iteration instead of reusing
EF Core's managed connection. This prevents connection disposal issues
when iterating over multiple tenant IDs from the JWT clubs claim.

The fix ensures each iteration has its own connection lifecycle with
proper SET LOCAL app.current_tenant_id for RLS compliance.
This commit is contained in:
WorkClub Automation
2026-03-05 21:46:19 +01:00
parent ffc4062eba
commit dbc8964f07

View File

@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
using WorkClub.Application.Clubs.DTOs; using WorkClub.Application.Clubs.DTOs;
using WorkClub.Application.Interfaces; using WorkClub.Application.Interfaces;
using WorkClub.Domain.Enums;
using WorkClub.Infrastructure.Data; using WorkClub.Infrastructure.Data;
namespace WorkClub.Api.Services; namespace WorkClub.Api.Services;
@@ -23,35 +25,99 @@ public class ClubService
public async Task<List<ClubListDto>> GetMyClubsAsync() public async Task<List<ClubListDto>> GetMyClubsAsync()
{ {
var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst("sub")?.Value; var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value;
if (string.IsNullOrEmpty(userIdClaim)) if (string.IsNullOrEmpty(clubsClaim))
{ {
return new List<ClubListDto>(); return new List<ClubListDto>();
} }
var memberships = await _context.Members var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Where(m => m.ExternalUserId == userIdClaim) .Select(t => t.Trim())
.ToListAsync(); .Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
.ToList();
var clubIds = memberships.Select(m => m.ClubId).ToList(); if (tenantIds.Count == 0)
{
var clubs = await _context.Clubs return new List<ClubListDto>();
.Where(c => clubIds.Contains(c.Id)) }
.ToListAsync();
var clubDtos = new List<ClubListDto>(); var clubDtos = new List<ClubListDto>();
foreach (var club in clubs) var connectionString = _context.Database.GetConnectionString();
{
var memberCount = await _context.Members
.Where(m => m.ClubId == club.Id)
.CountAsync();
clubDtos.Add(new ClubListDto( foreach (var tenantId in tenantIds)
club.Id, {
club.Name, await using var connection = new NpgsqlConnection(connectionString);
club.SportType.ToString(), await connection.OpenAsync();
memberCount
)); await using var transaction = await connection.BeginTransactionAsync();
// Set RLS context
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
await command.ExecuteNonQueryAsync();
}
Guid? clubId = null;
string? clubName = null;
int? sportTypeInt = null;
// Fetch club details
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = @"
SELECT c.""Id"", c.""Name"", c.""SportType""
FROM clubs AS c
WHERE c.""TenantId"" = @tenantId";
var parameter = command.CreateParameter();
parameter.ParameterName = "@tenantId";
parameter.Value = tenantId;
command.Parameters.Add(parameter);
using (var reader = await command.ExecuteReaderAsync())
{
if (await reader.ReadAsync())
{
clubId = reader.GetGuid(0);
clubName = reader.GetString(1);
sportTypeInt = reader.GetInt32(2);
}
}
}
// Fetch member count if club exists
if (clubId.HasValue && clubName != null && sportTypeInt.HasValue)
{
using (var memberCommand = connection.CreateCommand())
{
memberCommand.Transaction = transaction;
memberCommand.CommandText = @"
SELECT COUNT(*)
FROM members AS m
WHERE m.""ClubId"" = @clubId";
var param = memberCommand.CreateParameter();
param.ParameterName = "@clubId";
param.Value = clubId;
memberCommand.Parameters.Add(param);
var memberCountResult = await memberCommand.ExecuteScalarAsync();
var memberCount = memberCountResult != null ? Convert.ToInt32(memberCountResult) : 0;
var sportTypeEnum = ((SportType)sportTypeInt.Value).ToString();
clubDtos.Add(new ClubListDto(
clubId.Value,
clubName,
sportTypeEnum,
memberCount
));
}
}
await transaction.CommitAsync();
} }
return clubDtos; return clubDtos;