first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
namespace Fitness_core_BE.Infra.Tenancy
{
// Request-scoped tenant info, populated once per request by TenantResolutionAttribute
// (Helpers/TenantResolutionAttribute.cs) so downstream repos/services can read the
// caller's resolved TenantId instead of re-deriving ClientId ad hoc via UserRoleHelper.
public interface ITenantContext
{
// Client owner / Staff: their one gym. Customer: the gym validated from X-Tenant-Id
// for this request, if any was sent. Admin: always null (see IsPlatformAdmin).
Guid? TenantId { get; }
bool IsPlatformAdmin { get; }
bool IsResolved { get; }
void SetTenant(Guid? tenantId, bool isPlatformAdmin);
}
public class TenantContext : ITenantContext
{
public Guid? TenantId { get; private set; }
public bool IsPlatformAdmin { get; private set; }
public bool IsResolved { get; private set; }
public void SetTenant(Guid? tenantId, bool isPlatformAdmin)
{
TenantId = tenantId;
IsPlatformAdmin = isPlatformAdmin;
IsResolved = true;
}
}
}
+99
View File
@@ -0,0 +1,99 @@
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<Guid> 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<TenantSchemaContext?> 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<CentralDbContext>();
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<TenantDbContext>();
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 0)));
return new TenantDbContext(optionsBuilder.Options);
}
}
}
+75
View File
@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Storage;
namespace Fitness_core_BE.Infra.UoW
{
public class EFUnitOfWork : IUnitOfWork
{
// Central and tenant are separate physical connections -- each gets its own local
// transaction. There is no distributed-transaction coordinator wired up, so if one
// side's commit fails after the other has already committed, that is NOT rolled back
// atomically; true cross-DB atomicity is deferred to a saga/compensating-action
// pattern (see the migration plan's Phase 2 step 8). Both sides are always
// begun/committed/rolled-back together so ordinary success/failure flows (the vast
// majority of call sites) still behave exactly as the single-context version did.
private readonly CentralDbContext _central;
private readonly TenantDbContext _tenant;
private IDbContextTransaction? _centralTransaction;
private IDbContextTransaction? _tenantTransaction;
public EFUnitOfWork(CentralDbContext central, TenantDbContext tenant)
{
_central = central;
_tenant = tenant;
}
public async Task BeginAsync(CancellationToken ct = default)
{
if (_centralTransaction == null)
_centralTransaction = await _central.Database.BeginTransactionAsync(ct);
if (_tenantTransaction == null)
_tenantTransaction = await _tenant.Database.BeginTransactionAsync(ct);
}
public async Task SaveChangesAsync(CancellationToken ct = default)
{
await _central.SaveChangesAsync(ct);
await _tenant.SaveChangesAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
await _central.SaveChangesAsync(ct);
await _tenant.SaveChangesAsync(ct);
if (_centralTransaction != null)
{
await _centralTransaction.CommitAsync(ct);
await _centralTransaction.DisposeAsync();
_centralTransaction = null;
}
if (_tenantTransaction != null)
{
await _tenantTransaction.CommitAsync(ct);
await _tenantTransaction.DisposeAsync();
_tenantTransaction = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_centralTransaction != null)
{
await _centralTransaction.RollbackAsync(ct);
await _centralTransaction.DisposeAsync();
_centralTransaction = null;
}
if (_tenantTransaction != null)
{
await _tenantTransaction.RollbackAsync(ct);
await _tenantTransaction.DisposeAsync();
_tenantTransaction = null;
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Fitness_core_BE.Infra.UoW
{
public interface IUnitOfWork
{
Task BeginAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
}