< Summary

Information
Class: ArturRios.Data.Relational.Core.Transactions.EfUnitOfWork
Assembly: ArturRios.Data.Relational.Core
File(s): /home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Relational.Core/Transactions/EfUnitOfWork.cs
Line coverage
84%
Covered lines: 53
Uncovered lines: 10
Coverable lines: 63
Total lines: 162
Line coverage: 84.1%
Branch coverage
85%
Covered branches: 12
Total branches: 14
Branch coverage: 85.7%
Method coverage
77%
Covered methods: 14
Fully covered methods: 10
Total methods: 18
Method coverage: 77.7%
Full method coverage: 55.5%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteInTransactionAsync(...)100%1180%
ExecuteInTransactionAsync(...)100%22100%
BeginTransactionAsync()100%11100%
ExecuteInTransaction(...)100%11100%
ExecuteInTransaction(...)50%22100%
RunAsync()100%44100%
Run(...)83.33%6692.85%
RollbackQuietlyAsync()100%1166.66%
RollbackQuietly(...)100%1166.66%
BeginTransaction()100%11100%
.ctor(...)100%11100%
Commit()100%210%
Rollback()100%210%
CommitAsync(...)100%210%
RollbackAsync(...)100%210%
Dispose()100%11100%
DisposeAsync()100%11100%

File(s)

/home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Relational.Core/Transactions/EfUnitOfWork.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Configuration;
 2using ArturRios.Data.Relational.Core.Repositories;
 3using ArturRios.Output;
 4using Microsoft.EntityFrameworkCore.Storage;
 5
 6namespace 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///     Every failure - including one to begin the transaction - is returned as an error envelope; on rollback
 12///     the context's change tracker is cleared, since none of the transaction's writes were kept.
 13/// </summary>
 14/// <param name="context">The application's <see cref="BaseDbContext" />.</param>
 3015public class EfUnitOfWork(BaseDbContext context) : IUnitOfWork, IAsyncUnitOfWork
 16{
 17    /// <inheritdoc />
 18    public Task<ProcessOutput> ExecuteInTransactionAsync(Func<Task> work, CancellationToken ct = default) =>
 619        RunAsync(async () =>
 620        {
 421            await work().ConfigureAwait(false);
 022            return ProcessOutput.New;
 1023        }, error => ProcessOutput.New.WithError(error), ct);
 24
 25    /// <inheritdoc />
 26    public Task<DataOutput<TResult>> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> work,
 27        CancellationToken ct = default) =>
 1028        RunAsync(async () => DataOutput<TResult>.New.WithData(await work().ConfigureAwait(false)),
 829            error => DataOutput<TResult>.New.WithError(error), ct);
 30
 31    /// <inheritdoc />
 32    public async Task<IDbTransactionHandle> BeginTransactionAsync(CancellationToken ct = default) =>
 233        new EfTransactionHandle(await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false));
 34
 35    /// <inheritdoc />
 36    public ProcessOutput ExecuteInTransaction(Action work) =>
 1237        Run(() =>
 1238        {
 1039            work();
 240            return ProcessOutput.New;
 2241        }, error => ProcessOutput.New.WithError(error));
 42
 43    /// <inheritdoc />
 44    public DataOutput<TResult> ExecuteInTransaction<TResult>(Func<TResult> work) =>
 445        Run(() => DataOutput<TResult>.New.WithData(work()), error => DataOutput<TResult>.New.WithError(error));
 46
 47    // Every step that can fail - beginning the transaction (no connection, or one already open on the
 48    // context), the work and the commit - runs inside the guard, so a failure is enveloped like any
 49    // other instead of escaping.
 50    private async Task<TOutput> RunAsync<TOutput>(Func<Task<TOutput>> work, Func<string, TOutput> fail,
 51        CancellationToken ct)
 52    {
 1253        IDbContextTransaction? tx = null;
 54        try
 55        {
 1256            tx = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false);
 857            var output = await work().ConfigureAwait(false);
 458            await tx.CommitAsync(ct).ConfigureAwait(false);
 59
 460            return output;
 61        }
 862        catch (Exception ex)
 63        {
 864            if (tx is not null)
 65            {
 466                await RollbackQuietlyAsync(tx).ConfigureAwait(false);
 67            }
 68
 869            if (ex is OperationCanceledException)
 70            {
 271                throw;
 72            }
 73
 674            return fail(RelationalErrors.Describe(ex));
 75        }
 76        finally
 77        {
 1278            if (tx is not null)
 79            {
 880                await tx.DisposeAsync().ConfigureAwait(false);
 81            }
 82        }
 1083    }
 84
 85    private TOutput Run<TOutput>(Func<TOutput> work, Func<string, TOutput> fail)
 86    {
 1487        IDbContextTransaction? tx = null;
 88        try
 89        {
 1490            tx = context.Database.BeginTransaction();
 1091            var output = work();
 292            tx.Commit();
 93
 294            return output;
 95        }
 1296        catch (Exception ex)
 97        {
 1298            if (tx is not null)
 99            {
 8100                RollbackQuietly(tx);
 101            }
 102
 12103            if (ex is OperationCanceledException)
 104            {
 0105                throw;
 106            }
 107
 12108            return fail(RelationalErrors.Describe(ex));
 109        }
 110        finally
 111        {
 14112            tx?.Dispose();
 14113        }
 14114    }
 115
 116    // Rollback must never mask the failure that triggered it: it runs untied to the caller's
 117    // token (which may already be canceled, making Rollback throw before it rolls anything back)
 118    // and swallows its own errors. Disposing the transaction rolls back whatever is left.
 119    // The change tracker is cleared afterwards: it still holds the transaction's flushed writes as
 120    // saved (and its failed ones as pending), none of which the database kept.
 121    private async Task RollbackQuietlyAsync(IDbContextTransaction transaction)
 122    {
 123        try
 124        {
 4125            await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false);
 4126        }
 0127        catch
 128        {
 129            // Already rolled back, or the connection is gone. Dispose completes the cleanup.
 0130        }
 131
 4132        context.ChangeTracker.Clear();
 4133    }
 134
 135    private void RollbackQuietly(IDbContextTransaction transaction)
 136    {
 137        try
 138        {
 8139            transaction.Rollback();
 8140        }
 0141        catch
 142        {
 143            // Already rolled back, or the connection is gone. Dispose completes the cleanup.
 0144        }
 145
 8146        context.ChangeTracker.Clear();
 8147    }
 148
 149    /// <inheritdoc />
 150    public IDbTransactionHandle BeginTransaction() =>
 2151        new EfTransactionHandle(context.Database.BeginTransaction());
 152
 4153    private sealed class EfTransactionHandle(IDbContextTransaction transaction) : IDbTransactionHandle
 154    {
 0155        public void Commit() => transaction.Commit();
 0156        public void Rollback() => transaction.Rollback();
 0157        public Task CommitAsync(CancellationToken ct = default) => transaction.CommitAsync(ct);
 0158        public Task RollbackAsync(CancellationToken ct = default) => transaction.RollbackAsync(ct);
 2159        public void Dispose() => transaction.Dispose();
 2160        public ValueTask DisposeAsync() => transaction.DisposeAsync();
 161    }
 162}