| | | 1 | | using ArturRios.Data.Relational.Core.Configuration; |
| | | 2 | | using ArturRios.Data.Relational.Core.Repositories; |
| | | 3 | | using ArturRios.Output; |
| | | 4 | | using Microsoft.EntityFrameworkCore.Storage; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Data.Relational.Core.Transactions; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Entity Framework Core implementation of <see cref="IUnitOfWork" /> and <see cref="IAsyncUnitOfWork" />. |
| | | 10 | | /// Repository saves issued within the delegate flush but do not commit until the transaction commits. |
| | | 11 | | /// </summary> |
| | | 12 | | /// <param name="context">The application's <see cref="BaseDbContext" />.</param> |
| | 24 | 13 | | public class EfUnitOfWork(BaseDbContext context) : IUnitOfWork, IAsyncUnitOfWork |
| | | 14 | | { |
| | | 15 | | /// <inheritdoc /> |
| | | 16 | | public async Task<ProcessOutput> ExecuteInTransactionAsync(Func<Task> work, CancellationToken ct = default) |
| | | 17 | | { |
| | 4 | 18 | | var tx = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); |
| | | 19 | | |
| | 4 | 20 | | await using var txScope = tx.ConfigureAwait(false); |
| | | 21 | | |
| | | 22 | | try |
| | | 23 | | { |
| | 4 | 24 | | await work().ConfigureAwait(false); |
| | 0 | 25 | | await tx.CommitAsync(ct).ConfigureAwait(false); |
| | 0 | 26 | | return ProcessOutput.New; |
| | | 27 | | } |
| | 4 | 28 | | catch (Exception ex) |
| | | 29 | | { |
| | 4 | 30 | | await RollbackQuietlyAsync(tx).ConfigureAwait(false); |
| | | 31 | | |
| | 4 | 32 | | if (ex is OperationCanceledException) |
| | | 33 | | { |
| | 2 | 34 | | throw; |
| | | 35 | | } |
| | | 36 | | |
| | 2 | 37 | | return ProcessOutput.New.WithError(RelationalErrors.Describe(ex)); |
| | | 38 | | } |
| | 2 | 39 | | } |
| | | 40 | | |
| | | 41 | | /// <inheritdoc /> |
| | | 42 | | public async Task<DataOutput<TResult>> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> work, |
| | | 43 | | CancellationToken ct = default) |
| | | 44 | | { |
| | 4 | 45 | | var tx = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); |
| | | 46 | | |
| | 4 | 47 | | await using var txScope = tx.ConfigureAwait(false); |
| | | 48 | | |
| | | 49 | | try |
| | | 50 | | { |
| | 4 | 51 | | var result = await work().ConfigureAwait(false); |
| | 4 | 52 | | await tx.CommitAsync(ct).ConfigureAwait(false); |
| | | 53 | | |
| | 4 | 54 | | return DataOutput<TResult>.New.WithData(result); |
| | | 55 | | } |
| | 0 | 56 | | catch (Exception ex) |
| | | 57 | | { |
| | 0 | 58 | | await RollbackQuietlyAsync(tx).ConfigureAwait(false); |
| | | 59 | | |
| | 0 | 60 | | if (ex is OperationCanceledException) |
| | | 61 | | { |
| | 0 | 62 | | throw; |
| | | 63 | | } |
| | | 64 | | |
| | 0 | 65 | | return DataOutput<TResult>.New.WithError(RelationalErrors.Describe(ex)); |
| | | 66 | | } |
| | 4 | 67 | | } |
| | | 68 | | |
| | | 69 | | /// <inheritdoc /> |
| | | 70 | | public async Task<IDbTransactionHandle> BeginTransactionAsync(CancellationToken ct = default) => |
| | 0 | 71 | | new EfTransactionHandle(await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false)); |
| | | 72 | | |
| | | 73 | | /// <inheritdoc /> |
| | | 74 | | public ProcessOutput ExecuteInTransaction(Action work) |
| | | 75 | | { |
| | 8 | 76 | | using var tx = context.Database.BeginTransaction(); |
| | | 77 | | try |
| | | 78 | | { |
| | 8 | 79 | | work(); |
| | 2 | 80 | | tx.Commit(); |
| | | 81 | | |
| | 2 | 82 | | return ProcessOutput.New; |
| | | 83 | | } |
| | 0 | 84 | | catch (OperationCanceledException) |
| | | 85 | | { |
| | 0 | 86 | | RollbackQuietly(tx); |
| | | 87 | | |
| | 0 | 88 | | throw; |
| | | 89 | | } |
| | 6 | 90 | | catch (Exception ex) |
| | | 91 | | { |
| | 6 | 92 | | RollbackQuietly(tx); |
| | | 93 | | |
| | 6 | 94 | | return ProcessOutput.New.WithError(RelationalErrors.Describe(ex)); |
| | | 95 | | } |
| | 8 | 96 | | } |
| | | 97 | | |
| | | 98 | | /// <inheritdoc /> |
| | | 99 | | public DataOutput<TResult> ExecuteInTransaction<TResult>(Func<TResult> work) |
| | | 100 | | { |
| | 0 | 101 | | using var tx = context.Database.BeginTransaction(); |
| | | 102 | | try |
| | | 103 | | { |
| | 0 | 104 | | var result = work(); |
| | 0 | 105 | | tx.Commit(); |
| | | 106 | | |
| | 0 | 107 | | return DataOutput<TResult>.New.WithData(result); |
| | | 108 | | } |
| | 0 | 109 | | catch (OperationCanceledException) |
| | | 110 | | { |
| | 0 | 111 | | RollbackQuietly(tx); |
| | | 112 | | |
| | 0 | 113 | | throw; |
| | | 114 | | } |
| | 0 | 115 | | catch (Exception ex) |
| | | 116 | | { |
| | 0 | 117 | | RollbackQuietly(tx); |
| | | 118 | | |
| | 0 | 119 | | return DataOutput<TResult>.New.WithError(RelationalErrors.Describe(ex)); |
| | | 120 | | } |
| | 0 | 121 | | } |
| | | 122 | | |
| | | 123 | | // Rollback must never mask the failure that triggered it: it runs untied to the caller's |
| | | 124 | | // token (which may already be canceled, making Rollback throw before it rolls anything back) |
| | | 125 | | // and swallows its own errors. Disposing the transaction rolls back whatever is left. |
| | | 126 | | private static async Task RollbackQuietlyAsync(IDbContextTransaction transaction) |
| | | 127 | | { |
| | | 128 | | try |
| | | 129 | | { |
| | 4 | 130 | | await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false); |
| | 4 | 131 | | } |
| | 0 | 132 | | catch |
| | | 133 | | { |
| | | 134 | | // Already rolled back, or the connection is gone. Dispose completes the cleanup. |
| | 0 | 135 | | } |
| | 4 | 136 | | } |
| | | 137 | | |
| | | 138 | | private static void RollbackQuietly(IDbContextTransaction transaction) |
| | | 139 | | { |
| | | 140 | | try |
| | | 141 | | { |
| | 6 | 142 | | transaction.Rollback(); |
| | 6 | 143 | | } |
| | 0 | 144 | | catch |
| | | 145 | | { |
| | | 146 | | // Already rolled back, or the connection is gone. Dispose completes the cleanup. |
| | 0 | 147 | | } |
| | 6 | 148 | | } |
| | | 149 | | |
| | | 150 | | /// <inheritdoc /> |
| | | 151 | | public IDbTransactionHandle BeginTransaction() => |
| | 0 | 152 | | new EfTransactionHandle(context.Database.BeginTransaction()); |
| | | 153 | | |
| | 0 | 154 | | private sealed class EfTransactionHandle(IDbContextTransaction transaction) : IDbTransactionHandle |
| | | 155 | | { |
| | 0 | 156 | | public void Commit() => transaction.Commit(); |
| | 0 | 157 | | public void Rollback() => transaction.Rollback(); |
| | 0 | 158 | | public Task CommitAsync(CancellationToken ct = default) => transaction.CommitAsync(ct); |
| | 0 | 159 | | public Task RollbackAsync(CancellationToken ct = default) => transaction.RollbackAsync(ct); |
| | 0 | 160 | | public void Dispose() => transaction.Dispose(); |
| | 0 | 161 | | public ValueTask DisposeAsync() => transaction.DisposeAsync(); |
| | | 162 | | } |
| | | 163 | | } |