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.
146 lines
4.7 KiB
C#
146 lines
4.7 KiB
C#
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;
|
|
|
|
public class ClubService
|
|
{
|
|
private readonly AppDbContext _context;
|
|
private readonly ITenantProvider _tenantProvider;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
|
|
public ClubService(
|
|
AppDbContext context,
|
|
ITenantProvider tenantProvider,
|
|
IHttpContextAccessor httpContextAccessor)
|
|
{
|
|
_context = context;
|
|
_tenantProvider = tenantProvider;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
}
|
|
|
|
public async Task<List<ClubListDto>> GetMyClubsAsync()
|
|
{
|
|
var clubsClaim = _httpContextAccessor.HttpContext?.User.FindFirst("clubs")?.Value;
|
|
if (string.IsNullOrEmpty(clubsClaim))
|
|
{
|
|
return new List<ClubListDto>();
|
|
}
|
|
|
|
var tenantIds = clubsClaim.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(t => t.Trim())
|
|
.Where(t => !string.IsNullOrEmpty(t) && Guid.TryParse(t, out _))
|
|
.ToList();
|
|
|
|
if (tenantIds.Count == 0)
|
|
{
|
|
return new List<ClubListDto>();
|
|
}
|
|
|
|
var clubDtos = new List<ClubListDto>();
|
|
var connectionString = _context.Database.GetConnectionString();
|
|
|
|
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;
|
|
}
|
|
|
|
public async Task<ClubDetailDto?> GetCurrentClubAsync()
|
|
{
|
|
var tenantId = _tenantProvider.GetTenantId();
|
|
|
|
var club = await _context.Clubs
|
|
.FirstOrDefaultAsync(c => c.TenantId == tenantId);
|
|
|
|
if (club == null)
|
|
return null;
|
|
|
|
return new ClubDetailDto(
|
|
club.Id,
|
|
club.Name,
|
|
club.SportType.ToString(),
|
|
club.Description,
|
|
club.CreatedAt,
|
|
club.UpdatedAt
|
|
);
|
|
}
|
|
}
|