< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Information
Class: ZhEaIsNsAaBn.Infrastructure.Core.BaseUnitOfWork<T>
Assembly: ZhEaIsNsAaBn.Infrastructure.Core
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.Infrastructure.Core/UnitOfWork/BaseUnitOfWork.cs
Line coverage
98%
Covered lines: 73
Uncovered lines: 1
Coverable lines: 74
Total lines: 283
Line coverage: 98.6%
Branch coverage
75%
Covered branches: 15
Total branches: 20
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
BeginTransaction(...)100%22100%
BeginTransactionAsync()50%22100%
CommitTransaction(...)50%22100%
CommitTransactionAsync()100%22100%
ExecuteInTransactionAsync()75%44100%
GetFromSql(...)100%210%
GetFromSqlAsync()100%11100%
GetRepositoryInstance()100%11100%
RollbackTransaction(...)100%22100%
RollbackTransactionAsync()50%22100%
SaveChanges(...)100%11100%
SaveChangesAsync()100%11100%
GetDbContext(...)100%11100%
GetDbContextAsync(...)100%11100%
RunInTransactionAsync()100%22100%
Run(...)100%11100%
RunAsync()100%11100%
ToLayerResponse(...)50%22100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.Infrastructure.Core/UnitOfWork/BaseUnitOfWork.cs

#LineLine coverage
 1using System.Data;
 2
 3namespace ZhEaIsNsAaBn.Infrastructure.Core;
 4
 5/// <summary>
 6/// Base unit of work: hands out repositories from DI, saves the shared context in one batch
 7/// (<see cref="SaveChanges"/> / <see cref="SaveChangesAsync"/>) and runs transactions on it.
 8/// </summary>
 9/// <typeparam name="TDbContext">The application's context.</typeparam>
 10public abstract class BaseUnitOfWork< TDbContext > : IBaseUnitOfWork
 11    where TDbContext : DbContext
 12{
 13    private readonly IDbContextProvider<TDbContext> _dbContextProvider;
 14    private readonly IServiceProvider               _serviceProvider;
 15
 16
 17    /// <summary>Creates the unit of work.</summary>
 18    /// <param name="dbContextProvider">The context provider (same one the repositories use).</param>
 19    /// <param name="serviceProvider">The service provider repositories are resolved from.</param>
 20    /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception>
 6021    protected BaseUnitOfWork( IDbContextProvider<TDbContext> dbContextProvider, IServiceProvider serviceProvider )
 22    {
 6023        ArgumentNullException.ThrowIfNull( dbContextProvider );
 6024        ArgumentNullException.ThrowIfNull( serviceProvider );
 25
 6026        _dbContextProvider = dbContextProvider;
 6027        _serviceProvider   = serviceProvider;
 6028    }
 29
 30
 31    /// <summary>
 32    /// Starts a transaction on the shared context. Repositories that use the shared context write inside it until
 33    /// <see cref="CommitTransaction"/> or <see cref="RollbackTransaction"/>.
 34    /// </summary>
 35    /// <remarks>With a retrying execution strategy (<c>EnableRetryOnFailure</c>) use <see cref="ExecuteInTransactionAsy
 36    /// <param name="isolationLevel">The isolation level; <see cref="IsolationLevel.ReadCommitted"/> by default.</param>
 37    /// <param name="cancellationToken">Token to cancel waiting for the context.</param>
 38    /// <returns>Success, <see cref="EfExceptions.TransactionAlreadyStarted"/>, or the classified database error.</retur
 39    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 40    public LayerResponse BeginTransaction( IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationTok
 41    {
 1642        var dbContext = GetDbContext( cancellationToken );
 1643        var database  = dbContext.Database;
 44
 2045        if ( database.CurrentTransaction is not null ) { return EfExceptions.TransactionAlreadyStarted(); }
 46
 47
 1248        return ToLayerResponse( SafeExecution.ExecuteSafe( () => database.BeginTransaction( isolationLevel ) is not null
 49    }
 50
 51    /// <summary>Async version of <see cref="BeginTransaction"/>.</summary>
 52    /// <param name="isolationLevel">The isolation level; <see cref="IsolationLevel.ReadCommitted"/> by default.</param>
 53    /// <param name="cancellationToken">Token to cancel the operation.</param>
 54    /// <returns>Success, <see cref="EfExceptions.TransactionAlreadyStarted"/>, or the classified database error.</retur
 55    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 56    public async Task<LayerResponse> BeginTransactionAsync( IsolationLevel isolationLevel = IsolationLevel.ReadCommitted
 57    {
 1658        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 1659        var database  = dbContext.Database;
 60
 1661        if ( database.CurrentTransaction is not null ) { return EfExceptions.TransactionAlreadyStarted(); }
 62
 63
 1664        var begun = await SafeExecution.ExecuteSafeAsync( async () => await database.BeginTransactionAsync( isolationLev
 1665                                                          dbContext.GetDbErrorClassifier() )
 1666                                       .ConfigureAwait( false );
 67
 68
 1669        return ToLayerResponse( begun );
 1670    }
 71
 72    /// <summary>Commits the open transaction and closes it.</summary>
 73    /// <param name="cancellationToken">Token to cancel waiting for the context.</param>
 74    /// <returns>Success, <see cref="EfExceptions.NoActiveTransaction"/>, or the classified database error (the transact
 75    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 76    public LayerResponse CommitTransaction( CancellationToken cancellationToken = default )
 77    {
 478        var dbContext = GetDbContext( cancellationToken );
 79
 480        if ( dbContext.Database.CurrentTransaction is not { } transaction ) { return EfExceptions.NoActiveTransaction();
 81
 82
 483        try { return ToLayerResponse( SafeExecution.ExecuteSafe( () => Run( transaction.Commit ), dbContext.GetDbErrorCl
 884        finally { transaction.Dispose(); }
 485    }
 86
 87    /// <summary>Async version of <see cref="CommitTransaction"/>.</summary>
 88    /// <param name="cancellationToken">Token to cancel the operation.</param>
 89    /// <returns>Success, <see cref="EfExceptions.NoActiveTransaction"/>, or the classified database error (the transact
 90    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 91    public async Task<LayerResponse> CommitTransactionAsync( CancellationToken cancellationToken = default )
 92    {
 893        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 94
 1295        if ( dbContext.Database.CurrentTransaction is not { } transaction ) { return EfExceptions.NoActiveTransaction();
 96
 97
 498        await using ( transaction.ConfigureAwait( false ) )
 99        {
 4100            return ToLayerResponse( await SafeExecution.ExecuteSafeAsync( () => RunAsync( transaction.CommitAsync( cance
 4101                                                       .ConfigureAwait( false ) );
 102        }
 8103    }
 104
 105    /// <summary>
 106    /// Runs <paramref name="work"/> in a transaction on the shared context, through the context's execution strategy (s
 107    /// with <c>EnableRetryOnFailure</c>). Commits only when <paramref name="work"/> returns success; otherwise, or when
 108    /// rolls back.
 109    /// </summary>
 110    /// <remarks>
 111    /// On a transient failure the strategy runs <paramref name="work"/> again from the start, so it must be safe to rep
 112    /// (load, change and save inside it; don't rely on changes tracked before the call).
 113    /// </remarks>
 114    /// <param name="work">The work; it gets the cancellation token and returns success or the errors to roll back on.</
 115    /// <param name="isolationLevel">The isolation level; <see cref="IsolationLevel.ReadCommitted"/> by default.</param>
 116    /// <param name="cancellationToken">Token to cancel the operation.</param>
 117    /// <returns>
 118    /// The result of <paramref name="work"/>; <see cref="EfExceptions.TransactionAlreadyStarted"/>; or the classified e
 119    /// work, the commit or the strategy throws.
 120    /// </returns>
 121    /// <exception cref="ArgumentNullException"><paramref name="work"/> is <see langword="null"/>.</exception>
 122    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 123    public async Task<LayerResponse> ExecuteInTransactionAsync(
 124            Func<CancellationToken, Task<LayerResponse>> work, IsolationLevel isolationLevel = IsolationLevel.ReadCommit
 125        )
 126    {
 20127        ArgumentNullException.ThrowIfNull( work );
 128
 20129        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 130
 20131        if ( dbContext.Database.CurrentTransaction is not null ) { return EfExceptions.TransactionAlreadyStarted(); }
 132
 133
 20134        var strategy = dbContext.Database.CreateExecutionStrategy();
 135
 136
 20137        var result = await SafeExecution.ExecuteSafeAsync( () => strategy.ExecuteAsync( ( work, isolationLevel ), RunInT
 20138                                                           dbContext.GetDbErrorClassifier() )
 20139                                        .ConfigureAwait( false );
 140
 141
 16142        return result.IsSuccess ? result.Result! : LayerResponse.Failure( result.Errors );
 16143    }
 144
 145    /// <summary>Runs a stored procedure on the shared context; see <see cref="DbContextReadExtensions.GetFromSql{TEntit
 146    /// <typeparam name="T">A type in the model (usually keyless).</typeparam>
 147    /// <param name="storedName">The procedure: <c>proc</c>, <c>schema.proc</c> or <c>[schema].[proc]</c>; a constant, n
 148    /// <param name="cmdType">The <c>@CmdType</c> value; <see langword="null"/> to leave it out.</param>
 149    /// <param name="cancellationToken">Token to cancel waiting for the context.</param>
 150    /// <param name="parameters">The procedure parameters; keys are names with or without <c>@</c>.</param>
 151    /// <returns>The rows.</returns>
 152    /// <exception cref="ArgumentException"><paramref name="storedName"/> or a parameter name isn't a plain SQL identifi
 153    /// <exception cref="InvalidOperationException">No provider registered an <see cref="IStoredProcedureExecutor"/> for
 154    public IReadOnlyList<T> GetFromSql< T >( string storedName, int? cmdType = null, CancellationToken cancellationToken
 155        where T : class
 0156        => GetDbContext( cancellationToken ).GetFromSql<T>( storedName, cmdType, parameters );
 157
 158    /// <summary>Async version of <see cref="GetFromSql{T}"/>.</summary>
 159    /// <typeparam name="T">A type in the model (usually keyless).</typeparam>
 160    /// <param name="storedName">The procedure: <c>proc</c>, <c>schema.proc</c> or <c>[schema].[proc]</c>; a constant, n
 161    /// <param name="cmdType">The <c>@CmdType</c> value; <see langword="null"/> to leave it out.</param>
 162    /// <param name="cancellationToken">Token to cancel the operation.</param>
 163    /// <param name="parameters">The procedure parameters; keys are names with or without <c>@</c>.</param>
 164    /// <returns>The rows.</returns>
 165    /// <exception cref="ArgumentException"><paramref name="storedName"/> or a parameter name isn't a plain SQL identifi
 166    /// <exception cref="InvalidOperationException">No provider registered an <see cref="IStoredProcedureExecutor"/> for
 167    public async Task<IReadOnlyList<T>> GetFromSqlAsync< T >( string storedName, int? cmdType = null, CancellationToken 
 168        where T : class
 169    {
 4170        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 171
 172
 4173        return await dbContext.GetFromSqlAsync<T>( storedName, cmdType, cancellationToken, parameters ).ConfigureAwait( 
 4174    }
 175
 176    /// <inheritdoc />
 177    public virtual TRepository GetRepositoryInstance< TRepository >()
 178        where TRepository : notnull
 4179        => _serviceProvider.GetRequiredService<TRepository>();
 180
 181    /// <summary>Rolls back the open transaction and closes it.</summary>
 182    /// <param name="cancellationToken">Token to cancel waiting for the context.</param>
 183    /// <returns>Success, <see cref="EfExceptions.NoActiveTransaction"/>, or the classified database error.</returns>
 184    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 185    public LayerResponse RollbackTransaction( CancellationToken cancellationToken = default )
 186    {
 12187        var dbContext = GetDbContext( cancellationToken );
 188
 16189        if ( dbContext.Database.CurrentTransaction is not { } transaction ) { return EfExceptions.NoActiveTransaction();
 190
 191
 8192        try { return ToLayerResponse( SafeExecution.ExecuteSafe( () => Run( transaction.Rollback ), dbContext.GetDbError
 16193        finally { transaction.Dispose(); }
 8194    }
 195
 196    /// <summary>Async version of <see cref="RollbackTransaction"/>.</summary>
 197    /// <param name="cancellationToken">Token to cancel the operation.</param>
 198    /// <returns>Success, <see cref="EfExceptions.NoActiveTransaction"/>, or the classified database error.</returns>
 199    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 200    public async Task<LayerResponse> RollbackTransactionAsync( CancellationToken cancellationToken = default )
 201    {
 12202        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 203
 12204        if ( dbContext.Database.CurrentTransaction is not { } transaction ) { return EfExceptions.NoActiveTransaction();
 205
 206
 12207        await using ( transaction.ConfigureAwait( false ) )
 208        {
 12209            return ToLayerResponse( await SafeExecution.ExecuteSafeAsync( () => RunAsync( transaction.RollbackAsync( can
 12210                                                       .ConfigureAwait( false ) );
 211        }
 12212    }
 213
 214    /// <summary>Saves every pending change on the shared context.</summary>
 215    /// <param name="cancellationToken">Token to cancel waiting for the context.</param>
 216    /// <returns>The number of rows written, or the classified database error.</returns>
 217    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 218    public LayerResponse<int> SaveChanges( CancellationToken cancellationToken = default )
 219    {
 8220        var dbContext = GetDbContext( cancellationToken );
 221
 222
 8223        return SafeExecution.ExecuteSafe( () => dbContext.SaveChanges(), dbContext.GetDbErrorClassifier() );
 224    }
 225
 226    /// <summary>Async version of <see cref="SaveChanges"/>.</summary>
 227    /// <param name="cancellationToken">Token to cancel the save.</param>
 228    /// <returns>The number of rows written, or the classified database error.</returns>
 229    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was cancelled.</exception>
 230    public async Task<LayerResponse<int>> SaveChangesAsync( CancellationToken cancellationToken = default )
 231    {
 8232        var dbContext = await GetDbContextAsync( cancellationToken ).ConfigureAwait( false );
 233
 234
 8235        return await SafeExecution.ExecuteSafeAsync( () => dbContext.SaveChangesAsync( cancellationToken ), dbContext.Ge
 8236    }
 237
 238    /// <summary>Returns the shared context.</summary>
 239    /// <param name="cancellationToken">Token to cancel waiting for the provider.</param>
 240    /// <returns>The shared context.</returns>
 40241    protected virtual TDbContext GetDbContext( CancellationToken cancellationToken = default ) => _dbContextProvider.Get
 242
 243    /// <summary>Async version of <see cref="GetDbContext"/>.</summary>
 244    /// <param name="cancellationToken">Token to cancel the operation.</param>
 245    /// <returns>The shared context.</returns>
 68246    protected virtual Task<TDbContext> GetDbContextAsync( CancellationToken cancellationToken = default ) => _dbContextP
 247
 248
 249    private static async Task<LayerResponse> RunInTransactionAsync(
 250            DbContext dbContext, (Func<CancellationToken, Task<LayerResponse>> Work, IsolationLevel IsolationLevel) stat
 251        )
 252    {
 20253        var transaction = await dbContext.Database.BeginTransactionAsync( state.IsolationLevel, cancellationToken ).Conf
 254
 16255        await using ( transaction.ConfigureAwait( false ) )
 256        {
 16257            var result = await state.Work( cancellationToken ).ConfigureAwait( false );
 258
 259
 20260            if ( result.IsSuccess ) { await transaction.CommitAsync( cancellationToken ).ConfigureAwait( false ); }
 4261            else { await transaction.RollbackAsync( cancellationToken ).ConfigureAwait( false ); }
 262
 263
 12264            return result;
 265        }
 12266    }
 267
 268    private static bool Run( Action action )
 269    {
 12270        action();
 271
 12272        return true;
 273    }
 274
 275    private static async Task<bool> RunAsync( Task task )
 276    {
 16277        await task.ConfigureAwait( false );
 278
 16279        return true;
 16280    }
 281
 56282    private static LayerResponse ToLayerResponse( LayerResponse<bool> result ) => result.IsSuccess ? LayerResponse.Succe
 283}