From dbc8964f077e9334491e78e8d419bbdb91e2a7c6 Mon Sep 17 00:00:00 2001 From: WorkClub Automation Date: Thu, 5 Mar 2026 21:46:19 +0100 Subject: [PATCH] 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. --- backend/WorkClub.Api/Services/ClubService.cs | 108 +++++++++++++++---- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/backend/WorkClub.Api/Services/ClubService.cs b/backend/WorkClub.Api/Services/ClubService.cs index 6d7df8a..4578479 100644 --- a/backend/WorkClub.Api/Services/ClubService.cs +++ b/backend/WorkClub.Api/Services/ClubService.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; using WorkClub.Application.Clubs.DTOs; using WorkClub.Application.Interfaces; +using WorkClub.Domain.Enums; using WorkClub.Infrastructure.Data; namespace WorkClub.Api.Services; @@ -23,35 +25,99 @@ public class ClubService public async Task> GetMyClubsAsync() { - var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst("sub")?.Value; - if (string.IsNullOrEmpty(userIdClaim)) + var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value; + if (string.IsNullOrEmpty(clubsClaim)) { return new List(); } - var memberships = await _context.Members - .Where(m => m.ExternalUserId == userIdClaim) - .ToListAsync(); + var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _)) + .ToList(); - var clubIds = memberships.Select(m => m.ClubId).ToList(); - - var clubs = await _context.Clubs - .Where(c => clubIds.Contains(c.Id)) - .ToListAsync(); + if (tenantIds.Count == 0) + { + return new List(); + } var clubDtos = new List(); - foreach (var club in clubs) - { - var memberCount = await _context.Members - .Where(m => m.ClubId == club.Id) - .CountAsync(); + var connectionString = _context.Database.GetConnectionString(); - clubDtos.Add(new ClubListDto( - club.Id, - club.Name, - club.SportType.ToString(), - memberCount - )); + foreach (var tenantId in tenantIds) + { + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(); + + 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;