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

Information
Class: ZhEaIsNsAaBn.Infrastructure.Core.AuditInterceptor<T>
Assembly: ZhEaIsNsAaBn.Infrastructure.Core
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.Infrastructure.Core/Auditing/AuditInterceptor.cs
Line coverage
95%
Covered lines: 81
Uncovered lines: 4
Coverable lines: 85
Total lines: 219
Line coverage: 95.2%
Branch coverage
78%
Covered branches: 64
Total branches: 82
Branch coverage: 78%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.get_IdType()100%11100%
SavingChanges(...)0%620%
SavingChangesAsync(...)50%22100%
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.Stamp()100%11100%
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.IsHardDelete(...)100%11100%
Stamp()50%88100%
Apply(...)90%2020100%
SetCreator(...)100%11100%
KeepOrSetCreator(...)80%1010100%
SoftDelete(...)75%161695%
Set(...)100%1212100%
Keep(...)87.5%88100%
Current(...)50%44100%
Mapped(...)100%11100%

File(s)

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

#LineLine coverage
 1using Microsoft.EntityFrameworkCore.Diagnostics;
 2
 3namespace ZhEaIsNsAaBn.Infrastructure.Core;
 4
 5/// <summary>
 6/// Stamps the audit fields on <c>SaveChanges</c>, for one context (added to its options by <c>WithAuditing&lt;TId&gt;()
 7/// <c>UseAuditing&lt;TId&gt;()</c>). Fields come from the Domain interfaces or the Domain attributes; entities without 
 8/// <list type="bullet">
 9/// <item><b>Added:</b> <c>CreatedDate</c>, <c>CreatedById</c>, <c>CreatedByUser</c>. Without a current user, a creator 
 10/// (non-default) is kept; an id without a name gets the id's text.</item>
 11/// <item><b>Modified:</b> <c>UpdatedDate</c>, <c>UpdatedById</c>, <c>UpdatedByUser</c>. The created fields, the soft-de
 12/// deleted fields are <b>never written by an update</b> (a disconnected <c>Update( entity )</c> can't reset them, and a
 13/// soft-deleted row back; use <c>Restore[Async]</c>).</item>
 14/// <item><b>Deleted:</b> soft delete — the flag, <c>DeletedDate</c>, <c>DeletedById</c>, <c>DeletedByUser</c> — when th
 15/// soft-delete flag and isn't <c>HardDelete&lt;T&gt;()</c>; otherwise a real delete. Properties edited before <c>Remove
 16/// it (the created fields stay protected).</item>
 17/// </list>
 18/// The user is <see cref="ICurrentUserProvider{TId}"/>'s, else the system user; the time is the <see cref="TimeProvider
 19/// </summary>
 20/// <typeparam name="TId">The user key type.</typeparam>
 21public sealed class AuditInterceptor< TId > : SaveChangesInterceptor, IAuditStamper
 22    where TId : struct, IEquatable<TId>
 23{
 24    private readonly AuditOptions<TId>          _options;
 25    private readonly ICurrentUserProvider<TId>? _currentUser;
 26
 27
 28    /// <summary>Creates the interceptor.</summary>
 29    /// <param name="options">The settings; <see cref="AuditOptions{TId}.SystemUser"/> must have been called.</param>
 30    /// <param name="currentUser">The current user; <see langword="null"/> stamps the system user always.</param>
 31    /// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
 32    /// <exception cref="InvalidOperationException">No system user is set.</exception>
 1233    public AuditInterceptor( AuditOptions<TId> options, ICurrentUserProvider<TId>? currentUser = null )
 34    {
 1235        ArgumentNullException.ThrowIfNull( options );
 36
 1237        options.Validate();
 38
 1239        _options     = options;
 1240        _currentUser = currentUser;
 1241    }
 42
 43
 16844    Type IAuditStamper.IdType => typeof( TId );
 45
 46
 47    /// <inheritdoc />
 48    public override InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result 
 49    {
 050        ArgumentNullException.ThrowIfNull( eventData );
 51
 052        if ( eventData.Context is { } context ) { Apply( context ); }
 53
 54
 055        return result;
 56    }
 57
 58    /// <inheritdoc />
 59    public override ValueTask<InterceptionResult<int>> SavingChangesAsync( DbContextEventData eventData, InterceptionRes
 60    {
 16061        ArgumentNullException.ThrowIfNull( eventData );
 62
 32063        if ( eventData.Context is { } context ) { Apply( context ); }
 64
 65
 16066        return ValueTask.FromResult( result );
 67    }
 68
 69
 2470    AuditStamp IAuditStamper.Stamp() => Stamp();
 71
 872    bool IAuditStamper.IsHardDelete( Type entityType ) => _options.IsHardDelete( entityType );
 73
 74
 75    private AuditStamp Stamp()
 76    {
 18477        var now = _options.TimeProvider.GetLocalNow().DateTime;
 78
 18479        return _currentUser?.Id is { } id
 18480                   ? new AuditStamp( now, id, _currentUser.Name ?? id.ToString() ?? string.Empty )
 18481                   : new AuditStamp( now, _options.SystemUserId!.Value, _options.SystemUserName! );
 82    }
 83
 84    private void Apply( DbContext context )
 85    {
 86        // Entries() runs DetectChanges first; materialise, because soft deletes change states while we walk.
 16087        var entries = context.ChangeTracker.Entries().Where( e => e.State is EntityState.Added or EntityState.Modified o
 88
 16089        if ( entries.Count == 0 ) { return; }
 90
 91
 16092        var stamp   = Stamp();
 16093        var hasUser = _currentUser?.Id is not null;
 94
 71295        foreach ( var entry in entries )
 96        {
 19697            var map = AuditMap.For( entry.Metadata.ClrType );
 98
 19699            if ( map.IsEmpty ) { continue; }
 100
 101
 172102            switch ( entry.State )
 103            {
 104                case EntityState.Added:
 108105                    Set( entry, map, AuditRole.CreatedDate, stamp.Now );
 106
 200107                    if ( hasUser ) { SetCreator( entry, map, stamp.UserId, stamp.UserName ); }
 16108                    else { KeepOrSetCreator( entry, map, stamp ); }
 109
 16110                    break;
 111
 112                case EntityState.Modified:
 20113                    Keep( entry, map, AuditRole.CreatedDate, AuditRole.CreatedById, AuditRole.CreatedByUser,
 20114                          AuditRole.SoftDeleteFlag, AuditRole.DeletedDate, AuditRole.DeletedById, AuditRole.DeletedByUse
 115
 20116                    Set( entry, map, AuditRole.UpdatedDate, stamp.Now );
 20117                    Set( entry, map, AuditRole.UpdatedById, stamp.UserId );
 20118                    Set( entry, map, AuditRole.UpdatedByUser, stamp.UserName );
 119
 20120                    break;
 121
 44122                case EntityState.Deleted when map.SoftDeleteFlag is not null && Mapped( entry, map.SoftDeleteFlag.Name )
 36123                    SoftDelete( context, entry, map, stamp );
 124
 125                    break;
 126            }
 127        }
 160128    }
 129
 130    private void SetCreator( EntityEntry entry, AuditMap map, object userId, string userName )
 131    {
 108132        Set( entry, map, AuditRole.CreatedById, userId );
 108133        Set( entry, map, AuditRole.CreatedByUser, userName );
 108134    }
 135
 136    /// No current user: a creator the app already set (e.g. a background job) is kept; a missing id is the system user'
 137    /// name is the kept id's text, else the system user's.
 138    private void KeepOrSetCreator( EntityEntry entry, AuditMap map, AuditStamp stamp )
 139    {
 16140        var id   = Current( entry, map, AuditRole.CreatedById );
 16141        var name = Current( entry, map, AuditRole.CreatedByUser ) as string;
 142
 16143        var keepId   = id is not null && !Equals( id, default( TId ) );
 16144        var keepName = !string.IsNullOrEmpty( name );
 145
 146
 16147        SetCreator( entry,
 16148                    map,
 16149                    keepId ? id! : stamp.UserId,
 16150                    keepName ? name! : keepId ? Convert.ToString( id, CultureInfo.InvariantCulture ) ?? string.Empty : s
 16151    }
 152
 153    private void SoftDelete( DbContext context, EntityEntry entry, AuditMap map, AuditStamp stamp )
 154    {
 155        // Edits made before Remove() are saved with the soft delete; the created fields stay protected.
 36156        var edited = entry.Properties
 36157                          .Where( p => !p.Metadata.IsPrimaryKey() && !p.Metadata.GetValueComparer().Equals( p.CurrentVal
 36158                          .Select( p => p.Metadata.Name )
 36159                          .ToList();
 160
 161        // Unchanged first, so only the delete fields and the edited ones are written (not every column).
 36162        entry.State = EntityState.Unchanged;
 163
 96164        foreach ( var name in edited ) { entry.Property( name ).IsModified = true; }
 165
 36166        Keep( entry, map, AuditRole.CreatedDate, AuditRole.CreatedById, AuditRole.CreatedByUser );
 167
 36168        Set( entry, map, AuditRole.SoftDeleteFlag, true );
 36169        Set( entry, map, AuditRole.DeletedDate, stamp.Now );
 36170        Set( entry, map, AuditRole.DeletedById, stamp.UserId );
 36171        Set( entry, map, AuditRole.DeletedByUser, stamp.UserName );
 172
 173
 174        // Owned dependents were marked Deleted with their owner; they stay with the soft-deleted row.
 88175        foreach ( var navigation in entry.Navigations.Where( n => n.Metadata.TargetEntityType.IsOwned() ) )
 176        {
 8177            var owned = navigation switch
 8178                            {
 4179                                ReferenceEntry reference   => reference.CurrentValue is null ? Array.Empty<object>() : [
 4180                                CollectionEntry collection => collection.CurrentValue?.Cast<object>().ToArray() ?? Array
 0181                                _                          => Array.Empty<object>()
 8182                            };
 183
 52184            foreach ( var dependent in owned.Select( context.Entry ).Where( d => d.State == EntityState.Deleted ) ) { de
 185        }
 36186    }
 187
 188    private void Set( EntityEntry entry, AuditMap map, AuditRole role, object value )
 189    {
 568190        if ( map[ role ] is not { } property || !Mapped( entry, property.Name ) ) { return; }
 191
 632192        if ( role is AuditRole.CreatedById or AuditRole.UpdatedById or AuditRole.DeletedById ) { ( (IAuditStamper) this 
 193
 194
 488195        entry.Property( property.Name ).CurrentValue = value;
 488196    }
 197
 198    private static void Keep( EntityEntry entry, AuditMap map, params AuditRole[] roles )
 199    {
 608200        foreach ( var role in roles )
 201        {
 248202            if ( map[ role ] is not { } property || !Mapped( entry, property.Name ) ) { continue; }
 203
 204
 240205            var value = entry.Property( property.Name );
 206
 240207            if ( !value.IsModified ) { continue; }
 208
 209
 52210            value.CurrentValue = value.OriginalValue;
 52211            value.IsModified   = false;
 212        }
 56213    }
 214
 215    private static object? Current( EntityEntry entry, AuditMap map, AuditRole role )
 32216        => map[ role ] is { } property && Mapped( entry, property.Name ) ? entry.Property( property.Name ).CurrentValue 
 217
 800218    private static bool Mapped( EntityEntry entry, string propertyName ) => entry.Metadata.FindProperty( propertyName ) 
 219}

Methods/Properties

.ctor(ZhEaIsNsAaBn.Infrastructure.Core.AuditOptions`1<TId>,ZhEaIsNsAaBn.Infrastructure.Core.ICurrentUserProvider`1<TId>)
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.get_IdType()
SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Int32>)
SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData,Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult`1<System.Int32>,System.Threading.CancellationToken)
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.Stamp()
ZhEaIsNsAaBn.Infrastructure.Core.IAuditStamper.IsHardDelete(System.Type)
Stamp()
Apply(Microsoft.EntityFrameworkCore.DbContext)
SetCreator(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,System.Object,System.String)
KeepOrSetCreator(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditStamp)
SoftDelete(Microsoft.EntityFrameworkCore.DbContext,Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditStamp)
Set(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditRole,System.Object)
Keep(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditRole[])
Current(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,ZhEaIsNsAaBn.Infrastructure.Core.AuditMap,ZhEaIsNsAaBn.Infrastructure.Core.AuditRole)
Mapped(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,System.String)