fix(transaction): preserve primary failures

Keep action and commit exceptions when rollback fails; clear owned transaction state and cover the lifecycle with SQLite tests.

Ref: IT-1588
This commit is contained in:
2026-10-15 13:33:52 +03:00
parent 1da2dd8579
commit 93d9ae3f9b
8 changed files with 324 additions and 41 deletions
+29 -40
View File
@@ -27,71 +27,60 @@ public class EfUnitOfWork<TDbContext> : IUnitOfWork
return _currentTransaction;
}
public async Task ExecuteInTransactionAsync(Func<Task> action)
public Task ExecuteInTransactionAsync(Func<Task> action)
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync();
try
return ExecuteInTransactionAsync(async () =>
{
await action();
if (ownsTransaction)
{
await tx.CommitAsync();
}
}
catch
{
if (ownsTransaction)
{
await tx.RollbackAsync();
}
throw;
}
finally
{
if (ownsTransaction)
{
await tx.DisposeAsync();
}
}
return true;
});
}
public async Task<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> action)
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
if (existing is not null)
{
return await action();
}
ITransaction tx = existing ?? await BeginTransactionAsync();
ITransaction tx = await BeginTransactionAsync();
try
{
TResult result = await action();
if (ownsTransaction)
{
await tx.CommitAsync();
}
await tx.CommitAsync();
return result;
}
catch
{
if (ownsTransaction)
{
await tx.RollbackAsync();
}
await TryRollbackAsync(tx);
throw;
}
finally
{
if (ownsTransaction)
try
{
await tx.DisposeAsync();
}
finally
{
_currentTransaction = null;
}
}
}
private static async Task TryRollbackAsync(ITransaction transaction)
{
try
{
await transaction.RollbackAsync();
}
catch
{
// Rollback can fail when the provider has already completed the transaction.
// Preserve the action or commit exception that caused the rollback attempt.
}
}