Files
tiku-backend.net/Tiku.Infrastructure/Tenancy/TenantExecutionScope.cs

133 lines
5.4 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Security;
using System.Diagnostics;
using System.Text.Json;
using Tiku.Domain.Operations;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Tenancy;
public sealed class TenantExecutionScope(
IServiceScopeFactory scopeFactory,
ILogger<TenantExecutionScope> logger) : ITenantExecutionScope
{
public Task ExecuteAsync(
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task> operation,
CancellationToken cancellationToken = default) =>
ExecuteAsync<object?>(request, async (provider, token) =>
{
await operation(provider, token);
return null;
}, cancellationToken);
public async Task<TResult> ExecuteAsync<TResult>(
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task<TResult>> operation,
CancellationToken cancellationToken = default)
{
Validate(request, operation);
await using var scope = scopeFactory.CreateAsyncScope();
var initializer = scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>();
initializer.InitializeSystem(request.TargetTenantId, request.Reason);
logger.LogWarning(
"Entering audited system scope. CallerType={CallerType} Caller={Caller} TargetTenantId={TargetTenantId} CorrelationId={CorrelationId} Reason={Reason}",
request.CallerType,
request.Caller,
request.TargetTenantId,
request.CorrelationId,
request.Reason);
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
var started = DateTimeOffset.UtcNow;
var stopwatch = Stopwatch.StartNew();
await using var transaction = dbContext.Database.IsRelational()
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
: null;
await WriteAuditAsync(dbContext, request, "system_scope.entered", started, null, null, cancellationToken);
try
{
var result = await operation(scope.ServiceProvider, cancellationToken);
await WriteAuditAsync(
dbContext, request, "system_scope.completed", started, stopwatch.ElapsedMilliseconds, null, cancellationToken);
if (transaction is not null)
{
await transaction.CommitAsync(cancellationToken);
}
return result;
}
catch (Exception exception)
{
if (transaction is not null)
{
await transaction.RollbackAsync(CancellationToken.None);
dbContext.ChangeTracker.Clear();
await WriteAuditAsync(
dbContext, request, "system_scope.entered", started, null, null, CancellationToken.None);
}
await WriteAuditAsync(
dbContext, request, "system_scope.failed", started, stopwatch.ElapsedMilliseconds,
exception.GetType().Name, CancellationToken.None);
throw;
}
}
private static void Validate<TResult>(
SystemScopeRequest request,
Func<IServiceProvider, CancellationToken, Task<TResult>> operation)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(operation);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Caller);
ArgumentException.ThrowIfNullOrWhiteSpace(request.Reason);
ArgumentException.ThrowIfNullOrWhiteSpace(request.CorrelationId);
if (request.TargetTenantId is null && !request.IsGlobal)
{
throw new ArgumentException("System scope requires a target tenant or an explicit global declaration.", nameof(request));
}
if (request.TargetTenantId is not null && request.IsGlobal)
{
throw new ArgumentException("A tenant-targeted system scope cannot also be global.", nameof(request));
}
if (request.IsGlobal && request.CallerType is SystemScopeCallerType.Worker or SystemScopeCallerType.PublicQuestionBank)
{
throw new ArgumentException("Worker and public question bank scopes must target a tenant.", nameof(request));
}
if (!Enum.IsDefined(request.CallerType))
{
throw new ArgumentOutOfRangeException(nameof(request), "Unknown system scope caller type.");
}
}
private static async Task WriteAuditAsync(
TikuDbContext dbContext,
SystemScopeRequest request,
string action,
DateTimeOffset startedAt,
long? elapsedMilliseconds,
string? failureType,
CancellationToken cancellationToken)
{
dbContext.AuditLogs.Add(new AuditLog
{
TenantId = request.TargetTenantId,
Action = action,
TargetType = "system_scope",
TargetId = request.CorrelationId,
Details = JsonSerializer.SerializeToElement(new
{
callerType = request.CallerType.ToString(),
request.Caller,
request.Reason,
request.CorrelationId,
request.IsGlobal,
startedAt,
elapsedMilliseconds,
failureType
})
});
await dbContext.SaveChangesAsync(cancellationToken);
}
}