< Summary

Information
Class: AsiBackbone.EntityFrameworkCore.Audit.EfCoreAuditResidueLifecycleStore
Assembly: AsiBackbone.EntityFrameworkCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.EntityFrameworkCore/Audit/EfCoreAuditResidueLifecycleStore.cs
Line coverage
97%
Covered lines: 72
Uncovered lines: 2
Coverable lines: 74
Total lines: 164
Line coverage: 97.2%
Branch coverage
50%
Covered branches: 4
Total branches: 8
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
AppendAsync()100%11100%
FindByEventIdAsync()50%22100%
FindByCorrelationIdAsync()100%11100%
FindByAuditResidueIdAsync()100%11100%
LifecycleEvents()100%11100%
ToEntity(...)100%11100%
ToLifecycleEvents(...)100%11100%
ToLifecycleEvent(...)100%11100%
DeserializeMetadata(...)50%6683.33%
EmptyMetadata()100%210%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.EntityFrameworkCore/Audit/EfCoreAuditResidueLifecycleStore.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using System.Text.Json;
 3using AsiBackbone.Core.Audit;
 4using AsiBackbone.EntityFrameworkCore.Persistence;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace AsiBackbone.EntityFrameworkCore.Audit;
 8
 9/// <summary>
 10/// Entity Framework Core-backed audit residue lifecycle store that persists records through a host-owned <see cref="DbC
 11/// </summary>
 12/// <remarks>
 13/// This store appends provider-neutral lifecycle events and intentionally relies on the host application to expose the 
 14/// </remarks>
 15public sealed class EfCoreAuditResidueLifecycleStore : IAsiBackboneAuditResidueLifecycleStore
 16{
 217    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
 18
 19    private readonly DbContext dbContext;
 20
 21    /// <summary>
 22    /// Initializes a new instance of the <see cref="EfCoreAuditResidueLifecycleStore" /> class.
 23    /// </summary>
 24    /// <param name="dbContext">The host-owned database context.</param>
 3625    public EfCoreAuditResidueLifecycleStore(DbContext dbContext)
 26    {
 3627        ArgumentNullException.ThrowIfNull(dbContext);
 28
 3629        this.dbContext = dbContext;
 3630    }
 31
 32    /// <inheritdoc />
 33    public async ValueTask<AuditResidueLifecycleEvent> AppendAsync(
 34        AuditResidueLifecycleEvent lifecycleEvent,
 35        CancellationToken cancellationToken = default)
 36    {
 3837        ArgumentNullException.ThrowIfNull(lifecycleEvent);
 3838        cancellationToken.ThrowIfCancellationRequested();
 39
 3840        _ = await dbContext
 3841            .Set<AsiBackboneAuditResidueLifecycleEventEntity>()
 3842            .AddAsync(ToEntity(lifecycleEvent), cancellationToken)
 3843            .ConfigureAwait(false);
 44
 3845        _ = await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 46
 3847        return lifecycleEvent;
 3848    }
 49
 50    /// <inheritdoc />
 51    public async ValueTask<AuditResidueLifecycleEvent?> FindByEventIdAsync(
 52        string eventId,
 53        CancellationToken cancellationToken = default)
 54    {
 255        ArgumentException.ThrowIfNullOrWhiteSpace(eventId);
 56
 257        string normalizedEventId = eventId.Trim();
 58
 259        AsiBackboneAuditResidueLifecycleEventEntity? entity = await LifecycleEvents()
 260            .Where(lifecycleEvent => lifecycleEvent.EventId == normalizedEventId)
 261            .SingleOrDefaultAsync(cancellationToken)
 262            .ConfigureAwait(false);
 63
 264        return entity is null ? null : ToLifecycleEvent(entity);
 265    }
 66
 67    /// <inheritdoc />
 68    public async ValueTask<IReadOnlyList<AuditResidueLifecycleEvent>> FindByCorrelationIdAsync(
 69        string correlationId,
 70        CancellationToken cancellationToken = default)
 71    {
 472        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 73
 474        string normalizedCorrelationId = correlationId.Trim();
 75
 476        List<AsiBackboneAuditResidueLifecycleEventEntity> entities = await LifecycleEvents()
 477            .Where(lifecycleEvent => lifecycleEvent.CorrelationId == normalizedCorrelationId)
 478            .ToListAsync(cancellationToken)
 479            .ConfigureAwait(false);
 80
 481        return [.. ToLifecycleEvents(entities)
 3682            .OrderBy(lifecycleEvent => lifecycleEvent.OccurredUtc)
 4083            .ThenBy(lifecycleEvent => lifecycleEvent.EventId, StringComparer.Ordinal)];
 484    }
 85
 86    /// <inheritdoc />
 87    public async ValueTask<IReadOnlyList<AuditResidueLifecycleEvent>> FindByAuditResidueIdAsync(
 88        string auditResidueId,
 89        CancellationToken cancellationToken = default)
 90    {
 291        ArgumentException.ThrowIfNullOrWhiteSpace(auditResidueId);
 92
 293        string normalizedAuditResidueId = auditResidueId.Trim();
 94
 295        List<AsiBackboneAuditResidueLifecycleEventEntity> entities = await LifecycleEvents()
 296            .Where(lifecycleEvent => lifecycleEvent.AuditResidueId == normalizedAuditResidueId)
 297            .ToListAsync(cancellationToken)
 298            .ConfigureAwait(false);
 99
 2100        return [.. ToLifecycleEvents(entities)
 4101            .OrderBy(lifecycleEvent => lifecycleEvent.OccurredUtc)
 6102            .ThenBy(lifecycleEvent => lifecycleEvent.EventId, StringComparer.Ordinal)];
 2103    }
 104
 105    private IQueryable<AsiBackboneAuditResidueLifecycleEventEntity> LifecycleEvents()
 106    {
 8107        return dbContext.Set<AsiBackboneAuditResidueLifecycleEventEntity>().AsNoTracking();
 108    }
 109
 110    private static AsiBackboneAuditResidueLifecycleEventEntity ToEntity(AuditResidueLifecycleEvent lifecycleEvent)
 111    {
 38112        return new AsiBackboneAuditResidueLifecycleEventEntity
 38113        {
 38114            EventId = lifecycleEvent.EventId,
 38115            Stage = lifecycleEvent.Stage,
 38116            StageSequence = lifecycleEvent.StageSequence,
 38117            OccurredUtc = lifecycleEvent.OccurredUtc,
 38118            CorrelationId = lifecycleEvent.CorrelationId,
 38119            AuditResidueId = lifecycleEvent.AuditResidueId,
 38120            TraceId = lifecycleEvent.TraceId,
 38121            OperationName = lifecycleEvent.OperationName,
 38122            Outcome = lifecycleEvent.Outcome,
 38123            MetadataJson = JsonSerializer.Serialize(lifecycleEvent.Metadata, JsonOptions)
 38124        };
 125    }
 126
 127    private static AuditResidueLifecycleEvent[] ToLifecycleEvents(IEnumerable<AsiBackboneAuditResidueLifecycleEventEntit
 128    {
 6129        return [.. entities.Select(ToLifecycleEvent)];
 130    }
 131
 132    private static AuditResidueLifecycleEvent ToLifecycleEvent(AsiBackboneAuditResidueLifecycleEventEntity entity)
 133    {
 42134        return AuditResidueLifecycleEvent.Create(
 42135            entity.Stage,
 42136            entity.CorrelationId,
 42137            entity.AuditResidueId,
 42138            entity.EventId,
 42139            entity.OccurredUtc,
 42140            entity.TraceId,
 42141            entity.OperationName,
 42142            entity.Outcome,
 42143            DeserializeMetadata(entity.MetadataJson));
 144    }
 145
 146    private static ReadOnlyDictionary<string, string> DeserializeMetadata(string? json)
 147    {
 42148        if (string.IsNullOrWhiteSpace(json))
 149        {
 0150            return EmptyMetadata();
 151        }
 152
 42153        Dictionary<string, string>? metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions)
 154
 42155        return metadata is null || metadata.Count == 0
 42156            ? EmptyMetadata()
 42157            : new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(metadata, StringComparer.Ordinal));
 158    }
 159
 160    private static ReadOnlyDictionary<string, string> EmptyMetadata()
 161    {
 0162        return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(StringComparer.Ordinal));
 163    }
 164}