< Summary

Information
Class: AsiBackbone.EntityFrameworkCore.Audit.EfCoreDecisionReceiptLifecycleStore
Assembly: AsiBackbone.EntityFrameworkCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.EntityFrameworkCore/Audit/EfCoreDecisionReceiptLifecycleStore.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%
FindByDecisionReceiptIdAsync()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/EfCoreDecisionReceiptLifecycleStore.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 decision receipt lifecycle store that persists records through a host-owned <see cref="
 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 EfCoreDecisionReceiptLifecycleStore : IDecisionReceiptLifecycleStore
 16{
 117    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="EfCoreDecisionReceiptLifecycleStore" /> class.
 23    /// </summary>
 24    /// <param name="dbContext">The host-owned database context.</param>
 1925    public EfCoreDecisionReceiptLifecycleStore(DbContext dbContext)
 26    {
 1927        ArgumentNullException.ThrowIfNull(dbContext);
 28
 1929        this.dbContext = dbContext;
 1930    }
 31
 32    /// <inheritdoc />
 33    public async ValueTask<DecisionReceiptLifecycleEvent> AppendAsync(
 34        DecisionReceiptLifecycleEvent lifecycleEvent,
 35        CancellationToken cancellationToken = default)
 36    {
 1937        ArgumentNullException.ThrowIfNull(lifecycleEvent);
 1938        cancellationToken.ThrowIfCancellationRequested();
 39
 1940        _ = await dbContext
 1941            .Set<DecisionReceiptLifecycleEventEntity>()
 1942            .AddAsync(ToEntity(lifecycleEvent), cancellationToken)
 1943            .ConfigureAwait(false);
 44
 1945        _ = await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 46
 1947        return lifecycleEvent;
 1948    }
 49
 50    /// <inheritdoc />
 51    public async ValueTask<DecisionReceiptLifecycleEvent?> FindByEventIdAsync(
 52        string eventId,
 53        CancellationToken cancellationToken = default)
 54    {
 155        ArgumentException.ThrowIfNullOrWhiteSpace(eventId);
 56
 157        string normalizedEventId = eventId.Trim();
 58
 159        DecisionReceiptLifecycleEventEntity? entity = await LifecycleEvents()
 160            .Where(lifecycleEvent => lifecycleEvent.EventId == normalizedEventId)
 161            .SingleOrDefaultAsync(cancellationToken)
 162            .ConfigureAwait(false);
 63
 164        return entity is null ? null : ToLifecycleEvent(entity);
 165    }
 66
 67    /// <inheritdoc />
 68    public async ValueTask<IReadOnlyList<DecisionReceiptLifecycleEvent>> FindByCorrelationIdAsync(
 69        string correlationId,
 70        CancellationToken cancellationToken = default)
 71    {
 272        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 73
 274        string normalizedCorrelationId = correlationId.Trim();
 75
 276        List<DecisionReceiptLifecycleEventEntity> entities = await LifecycleEvents()
 277            .Where(lifecycleEvent => lifecycleEvent.CorrelationId == normalizedCorrelationId)
 278            .ToListAsync(cancellationToken)
 279            .ConfigureAwait(false);
 80
 281        return [.. ToLifecycleEvents(entities)
 282            .OrderBy(lifecycleEvent => lifecycleEvent.OccurredUtc)
 283            .ThenBy(lifecycleEvent => lifecycleEvent.EventId, StringComparer.Ordinal)];
 284    }
 85
 86    /// <inheritdoc />
 87    public async ValueTask<IReadOnlyList<DecisionReceiptLifecycleEvent>> FindByDecisionReceiptIdAsync(
 88        string decisionReceiptId,
 89        CancellationToken cancellationToken = default)
 90    {
 191        ArgumentException.ThrowIfNullOrWhiteSpace(decisionReceiptId);
 92
 193        string normalizedDecisionReceiptId = decisionReceiptId.Trim();
 94
 195        List<DecisionReceiptLifecycleEventEntity> entities = await LifecycleEvents()
 196            .Where(lifecycleEvent => lifecycleEvent.DecisionReceiptId == normalizedDecisionReceiptId)
 197            .ToListAsync(cancellationToken)
 198            .ConfigureAwait(false);
 99
 1100        return [.. ToLifecycleEvents(entities)
 1101            .OrderBy(lifecycleEvent => lifecycleEvent.OccurredUtc)
 1102            .ThenBy(lifecycleEvent => lifecycleEvent.EventId, StringComparer.Ordinal)];
 1103    }
 104
 105    private IQueryable<DecisionReceiptLifecycleEventEntity> LifecycleEvents()
 106    {
 4107        return dbContext.Set<DecisionReceiptLifecycleEventEntity>().AsNoTracking();
 108    }
 109
 110    private static DecisionReceiptLifecycleEventEntity ToEntity(DecisionReceiptLifecycleEvent lifecycleEvent)
 111    {
 19112        return new DecisionReceiptLifecycleEventEntity
 19113        {
 19114            EventId = lifecycleEvent.EventId,
 19115            Stage = lifecycleEvent.Stage,
 19116            StageSequence = lifecycleEvent.StageSequence,
 19117            OccurredUtc = lifecycleEvent.OccurredUtc,
 19118            CorrelationId = lifecycleEvent.CorrelationId,
 19119            DecisionReceiptId = lifecycleEvent.DecisionReceiptId,
 19120            TraceId = lifecycleEvent.TraceId,
 19121            OperationName = lifecycleEvent.OperationName,
 19122            Outcome = lifecycleEvent.Outcome,
 19123            MetadataJson = JsonSerializer.Serialize(lifecycleEvent.Metadata, JsonOptions)
 19124        };
 125    }
 126
 127    private static DecisionReceiptLifecycleEvent[] ToLifecycleEvents(IEnumerable<DecisionReceiptLifecycleEventEntity> en
 128    {
 3129        return [.. entities.Select(ToLifecycleEvent)];
 130    }
 131
 132    private static DecisionReceiptLifecycleEvent ToLifecycleEvent(DecisionReceiptLifecycleEventEntity entity)
 133    {
 21134        return DecisionReceiptLifecycleEvent.Create(
 21135            entity.Stage,
 21136            entity.CorrelationId,
 21137            entity.DecisionReceiptId,
 21138            entity.EventId,
 21139            entity.OccurredUtc,
 21140            entity.TraceId,
 21141            entity.OperationName,
 21142            entity.Outcome,
 21143            DeserializeMetadata(entity.MetadataJson));
 144    }
 145
 146    private static ReadOnlyDictionary<string, string> DeserializeMetadata(string? json)
 147    {
 21148        if (string.IsNullOrWhiteSpace(json))
 149        {
 0150            return EmptyMetadata();
 151        }
 152
 21153        Dictionary<string, string>? metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions)
 154
 21155        return metadata is null || metadata.Count == 0
 21156            ? EmptyMetadata()
 21157            : 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}