using Microsoft.EntityFrameworkCore; namespace Fitness_core_BE.Infra.Tenancy { // A matched Central/Tenant context pair bound to one specific, already-known schema -- // built for callers that have no HttpContext to resolve a tenant from (background jobs), // so TenantConnectionResolver's request-based resolution can't apply. Dispose both // contexts together via `await using`. public sealed class TenantSchemaContext : IAsyncDisposable { public Guid ClientId { get; } public string SchemaName { get; } public CentralDbContext Central { get; } public TenantDbContext Tenant { get; } public TenantSchemaContext(Guid clientId, string schemaName, CentralDbContext central, TenantDbContext tenant) { ClientId = clientId; SchemaName = schemaName; Central = central; Tenant = tenant; } public async ValueTask DisposeAsync() { await Tenant.DisposeAsync(); await Central.DisposeAsync(); } } // Single club, single database -- every background job that used to loop over "every // provisioned tenant schema" now just runs once against the one Client row / one DB. Kept // as a class (rather than deleting it and rewriting every caller) so OverdueLockJob, // SubscriptionRenewalJob and AdminBackgroundJobsController keep working unchanged. public class TenantSchemaIterator { private readonly IConfiguration _configuration; public TenantSchemaIterator(IConfiguration configuration) { _configuration = configuration; } // Every client row (there's exactly one for Vijaya) -- callers open their own // TenantSchemaContext per entry. public async IAsyncEnumerable<(Guid ClientId, string SchemaName)> GetProvisionedRegistrationsAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default) { List clientIds; await using (var central = CreateCentralDbContext()) { clientIds = await central.Client.AsNoTracking().Select(c => c.ClientId).ToListAsync(ct); } foreach (var clientId in clientIds) yield return (clientId, "default"); } // One client's schema, when the caller already knows the ClientId (e.g. OverdueLockJob's // existing per-client loop from the central Client table). Null if the client doesn't exist. public async Task OpenSchemaForClientAsync(Guid clientId, CancellationToken ct = default) { bool exists; await using (var central = CreateCentralDbContext()) { exists = await central.Client.AsNoTracking().AnyAsync(c => c.ClientId == clientId, ct); } return exists ? OpenSchema(clientId, "default") : null; } // Opens a matched context pair for an already-known ClientId. Building the contexts // doesn't itself open a connection -- EF Core connects lazily on first query. public TenantSchemaContext OpenSchema(Guid clientId, string schemaName) { var central = CreateCentralDbContext(); var tenant = CreateTenantDbContext(); return new TenantSchemaContext(clientId, schemaName, central, tenant); } private CentralDbContext CreateCentralDbContext() { var connectionString = _configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("DefaultConnection is not configured."); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0))); return new CentralDbContext(optionsBuilder.Options); } private TenantDbContext CreateTenantDbContext() { var connectionString = _configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("DefaultConnection is not configured."); var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0))); return new TenantDbContext(optionsBuilder.Options); } } }