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

Information
Class: Microsoft.EntityFrameworkCore.AuditedBulkExtensions
Assembly: ZhEaIsNsAaBn.Infrastructure.Core
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.Infrastructure.Core/Auditing/AuditedBulkExtensions.cs
Line coverage
100%
Covered lines: 56
Uncovered lines: 0
Coverable lines: 56
Total lines: 207
Line coverage: 100%
Branch coverage
87%
Covered branches: 21
Total branches: 24
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AuditedExecuteUpdate(...)100%11100%
AuditedExecuteUpdateAsync(...)100%11100%
AuditedExecuteDelete(...)100%11100%
AuditedExecuteDeleteAsync(...)100%11100%
Restore(...)100%11100%
RestoreAsync(...)100%11100%
Update(...)100%11100%
Delete(...)100%44100%
RestorePlan(...)80%1010100%
StampUpdated(...)100%11100%
Stamp(...)87.5%88100%
Flag(...)100%22100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.Infrastructure.Core/Auditing/AuditedBulkExtensions.cs

#LineLine coverage
 1using System.Reflection;
 2using ZhEaIsNsAaBn.Infrastructure.Core;
 3
 4// ReSharper disable once CheckNamespace
 5namespace Microsoft.EntityFrameworkCore;
 6
 7/// <summary>
 8/// Set-based updates and deletes that stamp the same audit fields as <see cref="AuditInterceptor{TId}"/> (<c>ExecuteUpd
 9/// <c>ExecuteDelete</c> skip <c>SaveChanges</c>, so the interceptor never sees them).
 10/// <para>
 11/// Each method takes the <c>context</c> the query came from: an EF query doesn't expose its context, and the auditing s
 12/// (user, clock, hard-delete types) and error classifier belong to it. The context must have auditing (<c>WithAuditing&
 13/// otherwise <see cref="InvalidOperationException"/>. Database errors come back as <see cref="Error"/> (the <c>SafeExec
 14/// </para>
 15/// </summary>
 16public static class AuditedBulkExtensions
 17{
 18    /// <summary>Updates the matching rows with <paramref name="setters"/> and stamps <c>UpdatedDate</c> / <c>UpdatedByI
 19    /// <typeparam name="TEntity">The entity type.</typeparam>
 20    /// <param name="query">The rows, e.g. <c>db.Orders.Where( o =&gt; o.Expired )</c>.</param>
 21    /// <param name="context">The context the query came from.</param>
 22    /// <param name="setters">The columns to set, e.g. <c>s =&gt; s.SetProperty( o =&gt; o.Status, "Closed" )</c>.</para
 23    /// <returns>The number of rows updated, or the classified error.</returns>
 24    /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception>
 25    /// <exception cref="InvalidOperationException">The context has no auditing.</exception>
 26    public static LayerResponse<int> AuditedExecuteUpdate< TEntity >( this IQueryable<TEntity> query, DbContext context,
 27        where TEntity : class
 28    {
 829        var all = Update( query, context, setters );
 30
 431        return SafeExecution.ExecuteSafe( () => all.Execute( query ), context.GetDbErrorClassifier() );
 32    }
 33
 34    /// <inheritdoc cref="AuditedExecuteUpdate{TEntity}" />
 35    /// <param name="query">The rows.</param>
 36    /// <param name="context">The context the query came from.</param>
 37    /// <param name="setters">The columns to set.</param>
 38    /// <param name="cancellationToken">Token to cancel the command.</param>
 39    public static Task<LayerResponse<int>> AuditedExecuteUpdateAsync< TEntity >(
 40            this IQueryable<TEntity> query, DbContext context, Action<AuditedUpdateSetters<TEntity>> setters, Cancellati
 41        )
 42        where TEntity : class
 43    {
 844        var all = Update( query, context, setters );
 45
 846        return SafeExecution.ExecuteSafeAsync( () => all.ExecuteAsync( query, cancellationToken ), context.GetDbErrorCla
 47    }
 48
 49    /// <summary>
 50    /// Deletes the matching rows: a soft delete (flag, <c>DeletedDate</c>, <c>DeletedById</c>, <c>DeletedByUser</c>; ro
 51    /// are left as they are) when <typeparamref name="TEntity"/> has a soft-delete flag and isn't <c>HardDelete&lt;T&gt
 52    /// <c>ExecuteDelete</c>.
 53    /// </summary>
 54    /// <typeparam name="TEntity">The entity type.</typeparam>
 55    /// <param name="query">The rows.</param>
 56    /// <param name="context">The context the query came from.</param>
 57    /// <returns>The number of rows deleted (or flagged), or the classified error.</returns>
 58    /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception>
 59    /// <exception cref="InvalidOperationException">The context has no auditing.</exception>
 60    public static LayerResponse<int> AuditedExecuteDelete< TEntity >( this IQueryable<TEntity> query, DbContext context 
 61        where TEntity : class
 62    {
 463        var plan = Delete( query, context );
 64
 465        return SafeExecution.ExecuteSafe( () => plan.Setters is null ? query.ExecuteDelete() : plan.Setters.Execute( pla
 66    }
 67
 68    /// <inheritdoc cref="AuditedExecuteDelete{TEntity}" />
 69    /// <param name="query">The rows.</param>
 70    /// <param name="context">The context the query came from.</param>
 71    /// <param name="cancellationToken">Token to cancel the command.</param>
 72    public static Task<LayerResponse<int>> AuditedExecuteDeleteAsync< TEntity >( this IQueryable<TEntity> query, DbConte
 73        where TEntity : class
 74    {
 1275        var plan = Delete( query, context );
 76
 877        return SafeExecution.ExecuteSafeAsync( () => plan.Setters is null ? query.ExecuteDeleteAsync( cancellationToken 
 878                                               context.GetDbErrorClassifier() );
 79    }
 80
 81    /// <summary>
 82    /// Brings soft-deleted rows back: clears the flag and the deleted fields and stamps the updated fields. Only flagge
 83    /// <b>The soft-delete query filter hides them</b>, so pass a query without it, e.g. <c>db.Orders.IgnoreQueryFilters
 84    /// </summary>
 85    /// <typeparam name="TEntity">The entity type.</typeparam>
 86    /// <param name="query">The rows.</param>
 87    /// <param name="context">The context the query came from.</param>
 88    /// <returns>The number of rows restored, or the classified error.</returns>
 89    /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception>
 90    /// <exception cref="InvalidOperationException">The context has no auditing, or <typeparamref name="TEntity"/> has n
 91    public static LayerResponse<int> Restore< TEntity >( this IQueryable<TEntity> query, DbContext context )
 92        where TEntity : class
 93    {
 894        var (flagged, setters) = RestorePlan( query, context );
 95
 496        return SafeExecution.ExecuteSafe( () => setters.Execute( flagged ), context.GetDbErrorClassifier() );
 97    }
 98
 99    /// <inheritdoc cref="Restore{TEntity}" />
 100    /// <param name="query">The rows.</param>
 101    /// <param name="context">The context the query came from.</param>
 102    /// <param name="cancellationToken">Token to cancel the command.</param>
 103    public static Task<LayerResponse<int>> RestoreAsync< TEntity >( this IQueryable<TEntity> query, DbContext context, C
 104        where TEntity : class
 105    {
 4106        var (flagged, setters) = RestorePlan( query, context );
 107
 4108        return SafeExecution.ExecuteSafeAsync( () => setters.ExecuteAsync( flagged, cancellationToken ), context.GetDbEr
 109    }
 110
 111
 112    private static AuditedUpdateSetters<TEntity> Update< TEntity >( IQueryable<TEntity> query, DbContext context, Action
 113        where TEntity : class
 114    {
 16115        ArgumentNullException.ThrowIfNull( query );
 16116        ArgumentNullException.ThrowIfNull( context );
 16117        ArgumentNullException.ThrowIfNull( setters );
 118
 16119        var stamper = context.RequireAuditStamper();
 12120        var all     = new AuditedUpdateSetters<TEntity>();
 121
 122
 12123        setters( all );
 12124        StampUpdated( all, stamper, AuditMap.For( typeof( TEntity ) ), stamper.Stamp() );
 125
 12126        return all;
 127    }
 128
 129    private static (IQueryable<TEntity> Query, AuditedUpdateSetters<TEntity>? Setters) Delete< TEntity >( IQueryable<TEn
 130        where TEntity : class
 131    {
 16132        ArgumentNullException.ThrowIfNull( query );
 16133        ArgumentNullException.ThrowIfNull( context );
 134
 16135        var stamper = context.RequireAuditStamper();
 12136        var map     = AuditMap.For( typeof( TEntity ) );
 137
 20138        if ( map.SoftDeleteFlag is not { } flag || stamper.IsHardDelete( typeof( TEntity ) ) ) { return ( query, null );
 139
 140
 4141        var stamp   = stamper.Stamp();
 4142        var setters = new AuditedUpdateSetters<TEntity>();
 143
 4144        setters.SetAuditValue( flag, true );
 4145        Stamp( setters, stamper, map, AuditRole.DeletedDate, stamp.Now );
 4146        Stamp( setters, stamper, map, AuditRole.DeletedById, stamp.UserId );
 4147        Stamp( setters, stamper, map, AuditRole.DeletedByUser, stamp.UserName );
 148
 149
 4150        return ( query.Where( Flag<TEntity>( flag, isSet: false ) ), setters );
 151    }
 152
 153    private static (IQueryable<TEntity> Query, AuditedUpdateSetters<TEntity> Setters) RestorePlan< TEntity >( IQueryable
 154        where TEntity : class
 155    {
 12156        ArgumentNullException.ThrowIfNull( query );
 12157        ArgumentNullException.ThrowIfNull( context );
 158
 12159        var stamper = context.RequireAuditStamper();
 12160        var map     = AuditMap.For( typeof( TEntity ) );
 12161        var flag    = map.SoftDeleteFlag ?? throw new InvalidOperationException( $"{typeof( TEntity ).Name} has no soft-
 8162        var setters = new AuditedUpdateSetters<TEntity>();
 163
 8164        setters.SetAuditValue( flag, false );
 165
 64166        foreach ( var role in new[] { AuditRole.DeletedDate, AuditRole.DeletedById, AuditRole.DeletedByUser } )
 167        {
 168            // Back to null where the column allows it.
 24169            if ( map[ role ] is { } property && ( !property.PropertyType.IsValueType || Nullable.GetUnderlyingType( prop
 170            {
 24171                setters.SetAuditValue( property, null );
 172            }
 173        }
 174
 8175        StampUpdated( setters, stamper, map, stamper.Stamp() );
 176
 177
 8178        return ( query.Where( Flag<TEntity>( flag, isSet: true ) ), setters );
 179    }
 180
 181    private static void StampUpdated< TEntity >( AuditedUpdateSetters<TEntity> setters, IAuditStamper stamper, AuditMap 
 182        where TEntity : class
 183    {
 20184        Stamp( setters, stamper, map, AuditRole.UpdatedDate, stamp.Now );
 20185        Stamp( setters, stamper, map, AuditRole.UpdatedById, stamp.UserId );
 20186        Stamp( setters, stamper, map, AuditRole.UpdatedByUser, stamp.UserName );
 20187    }
 188
 189    private static void Stamp< TEntity >( AuditedUpdateSetters<TEntity> setters, IAuditStamper stamper, AuditMap map, Au
 190        where TEntity : class
 191    {
 72192        if ( map[ role ] is not { } property ) { return; }
 193
 96194        if ( role is AuditRole.UpdatedById or AuditRole.DeletedById ) { stamper.CheckIdType( typeof( TEntity ), property
 195
 196
 72197        setters.SetAuditValue( property, value );
 72198    }
 199
 200    private static Expression<Func<TEntity, bool>> Flag< TEntity >( PropertyInfo flag, bool isSet )
 201    {
 12202        var     row   = Expression.Parameter( typeof( TEntity ), "e" );
 12203        Expression value = Expression.Property( row, flag );
 204
 12205        return Expression.Lambda<Func<TEntity, bool>>( isSet ? value : Expression.Not( value ), row );
 206    }
 207}

Methods/Properties

AuditedExecuteUpdate(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext,System.Action`1<Microsoft.EntityFrameworkCore.AuditedUpdateSetters`1<TEntity>>)
AuditedExecuteUpdateAsync(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext,System.Action`1<Microsoft.EntityFrameworkCore.AuditedUpdateSetters`1<TEntity>>,System.Threading.CancellationToken)
AuditedExecuteDelete(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext)
AuditedExecuteDeleteAsync(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext,System.Threading.CancellationToken)
Restore(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext)
RestoreAsync(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext,System.Threading.CancellationToken)
Update(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext,System.Action`1<Microsoft.EntityFrameworkCore.AuditedUpdateSetters`1<TEntity>>)
Delete(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext)
RestorePlan(System.Linq.IQueryable`1<TEntity>,Microsoft.EntityFrameworkCore.DbContext)
StampUpdated(Microsoft.EntityFrameworkCore.AuditedUpdateSetters`1<TEntity>,ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditStamp)
Stamp(Microsoft.EntityFrameworkCore.AuditedUpdateSetters`1<TEntity>,ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditRole,System.Object)
Flag(System.Reflection.PropertyInfo,System.Boolean)