< Summary

Information
Class: AsiBackbone.EntityFrameworkCore.Audit.EfCoreAuditLedgerStore
Assembly: AsiBackbone.EntityFrameworkCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.EntityFrameworkCore/Audit/EfCoreAuditLedgerStore.cs
Line coverage
100%
Covered lines: 254
Uncovered lines: 0
Coverable lines: 254
Total lines: 425
Line coverage: 100%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
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%44100%
FindByRecordIdAsync()100%22100%
FindByCorrelationIdAsync()100%11100%
FindByTraceIdAsync()100%11100%
FindByActorIdAsync()100%11100%
FindByRecordedUtcRangeAsync()100%22100%
LedgerRecords()100%11100%
ToEntity(...)100%11100%
ToReasonCodeEntities(...)100%11100%
ToMetadataEntities(...)100%11100%
ToRecords(...)100%11100%
ToRecord(...)100%11100%
DeserializeReasonCodes(...)75%44100%
DeserializeMetadata(...)100%66100%
.ctor(...)100%11100%
get_EventId()100%11100%
get_AuditResidueId()100%11100%
get_SchemaVersion()100%11100%
get_OccurredUtc()100%11100%
get_ActorId()100%11100%
get_ActorType()100%11100%
get_ActorDisplayName()100%11100%
get_OperationName()100%11100%
get_Outcome()100%11100%
get_ReasonCodes()100%11100%
get_CorrelationId()100%11100%
get_TraceId()100%11100%
get_SpanId()100%11100%
get_ParentSpanId()100%11100%
get_DecisionLatencyMs()100%11100%
get_ConstraintSetHash()100%11100%
get_ConstraintCount()100%11100%
get_RiskScore()100%11100%
get_PolicyScope()100%11100%
get_TenantHash()100%11100%
get_OrganizationHash()100%11100%
get_EmitterStatus()100%11100%
get_EmitterProvider()100%11100%
get_OutboxSequence()100%11100%
get_GatewayExecutionId()100%11100%
get_DecisionStage()100%11100%
get_PolicyVersion()100%11100%
get_PolicyHash()100%11100%
get_Metadata()100%11100%

File(s)

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

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using System.Text.Json;
 3using AsiBackbone.Core.Actors;
 4using AsiBackbone.Core.Audit;
 5using AsiBackbone.Core.Results;
 6using AsiBackbone.EntityFrameworkCore.Persistence;
 7using Microsoft.EntityFrameworkCore;
 8
 9namespace AsiBackbone.EntityFrameworkCore.Audit;
 10
 11/// <summary>
 12/// Entity Framework Core-backed audit ledger store that persists records through a host-owned <see cref="DbContext" />.
 13/// </summary>
 14/// <remarks>
 15/// This store is append-oriented and intentionally relies on the host application to expose the ASI Backbone entities f
 16/// its own <see cref="DbContext" /> and migrations. It does not create a package-owned context or select a database pro
 17/// </remarks>
 18public sealed class EfCoreAuditLedgerStore : IAsiBackboneAuditLedgerStore
 19{
 220    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
 21
 22    private readonly DbContext dbContext;
 23
 24    /// <summary>
 25    /// Initializes a new instance of the <see cref="EfCoreAuditLedgerStore" /> class.
 26    /// </summary>
 27    /// <param name="dbContext">The host-owned database context.</param>
 1428    public EfCoreAuditLedgerStore(DbContext dbContext)
 29    {
 1430        ArgumentNullException.ThrowIfNull(dbContext);
 31
 1432        this.dbContext = dbContext;
 1433    }
 34
 35    /// <inheritdoc />
 36    public async ValueTask<OperationResult<AuditLedgerRecord>> AppendAsync(
 37        AuditLedgerRecord record,
 38        CancellationToken cancellationToken = default)
 39    {
 1440        ArgumentNullException.ThrowIfNull(record);
 1441        cancellationToken.ThrowIfCancellationRequested();
 42
 1443        AsiBackboneAuditLedgerRecordEntity entity = ToEntity(record);
 44
 1445        _ = await dbContext
 1446            .Set<AsiBackboneAuditLedgerRecordEntity>()
 1447            .AddAsync(entity, cancellationToken)
 1448            .ConfigureAwait(false);
 49
 4850        foreach (AsiBackboneAuditLedgerReasonCodeEntity reasonCode in ToReasonCodeEntities(entity.Id, record.ReasonCodes
 51        {
 1052            _ = await dbContext
 1053                .Set<AsiBackboneAuditLedgerReasonCodeEntity>()
 1054                .AddAsync(reasonCode, cancellationToken)
 1055                .ConfigureAwait(false);
 56        }
 57
 4458        foreach (AsiBackboneAuditLedgerMetadataEntity metadata in ToMetadataEntities(entity.Id, record.Metadata))
 59        {
 860            _ = await dbContext
 861                .Set<AsiBackboneAuditLedgerMetadataEntity>()
 862                .AddAsync(metadata, cancellationToken)
 863                .ConfigureAwait(false);
 64        }
 65
 66        try
 67        {
 1468            _ = await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 1269        }
 270        catch (DbUpdateException ex)
 71        {
 272            dbContext.ChangeTracker.Clear();
 73
 274            return OperationResult.Failure<AuditLedgerRecord>(
 275                "asi_backbone.audit_ledger.append_failed",
 276                ex.Message);
 77        }
 78
 1279        return OperationResult.Success(record);
 1480    }
 81
 82    /// <inheritdoc />
 83    public async ValueTask<AuditLedgerRecord?> FindByRecordIdAsync(
 84        string recordId,
 85        CancellationToken cancellationToken = default)
 86    {
 887        ArgumentException.ThrowIfNullOrWhiteSpace(recordId);
 88
 889        string normalizedRecordId = recordId.Trim();
 90
 891        AsiBackboneAuditLedgerRecordEntity? entity = await LedgerRecords()
 892            .Where(record => record.RecordId == normalizedRecordId)
 893            .SingleOrDefaultAsync(cancellationToken)
 894            .ConfigureAwait(false);
 95
 896        return entity is null ? null : ToRecord(entity);
 897    }
 98
 99    /// <inheritdoc />
 100    public async ValueTask<IReadOnlyList<AuditLedgerRecord>> FindByCorrelationIdAsync(
 101        string correlationId,
 102        CancellationToken cancellationToken = default)
 103    {
 2104        ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
 105
 2106        string normalizedCorrelationId = correlationId.Trim();
 107
 2108        List<AsiBackboneAuditLedgerRecordEntity> entities = await LedgerRecords()
 2109            .Where(record => record.CorrelationId == normalizedCorrelationId)
 2110            .OrderBy(record => record.RecordedUtc)
 2111            .ThenBy(record => record.RecordId)
 2112            .ToListAsync(cancellationToken)
 2113            .ConfigureAwait(false);
 114
 2115        return ToRecords(entities);
 2116    }
 117
 118    /// <inheritdoc />
 119    public async ValueTask<IReadOnlyList<AuditLedgerRecord>> FindByTraceIdAsync(
 120        string traceId,
 121        CancellationToken cancellationToken = default)
 122    {
 2123        ArgumentException.ThrowIfNullOrWhiteSpace(traceId);
 124
 2125        string normalizedTraceId = traceId.Trim();
 126
 2127        List<AsiBackboneAuditLedgerRecordEntity> entities = await LedgerRecords()
 2128            .Where(record => record.TraceId == normalizedTraceId)
 2129            .OrderBy(record => record.RecordedUtc)
 2130            .ThenBy(record => record.RecordId)
 2131            .ToListAsync(cancellationToken)
 2132            .ConfigureAwait(false);
 133
 2134        return ToRecords(entities);
 2135    }
 136
 137    /// <inheritdoc />
 138    public async ValueTask<IReadOnlyList<AuditLedgerRecord>> FindByActorIdAsync(
 139        string actorId,
 140        CancellationToken cancellationToken = default)
 141    {
 2142        ArgumentException.ThrowIfNullOrWhiteSpace(actorId);
 143
 2144        string normalizedActorId = actorId.Trim();
 145
 2146        List<AsiBackboneAuditLedgerRecordEntity> entities = await LedgerRecords()
 2147            .Where(record => record.ActorId == normalizedActorId)
 2148            .OrderBy(record => record.RecordedUtc)
 2149            .ThenBy(record => record.RecordId)
 2150            .ToListAsync(cancellationToken)
 2151            .ConfigureAwait(false);
 152
 2153        return ToRecords(entities);
 2154    }
 155
 156    /// <inheritdoc />
 157    public async ValueTask<IReadOnlyList<AuditLedgerRecord>> FindByRecordedUtcRangeAsync(
 158        DateTimeOffset recordedFromUtc,
 159        DateTimeOffset recordedToUtc,
 160        CancellationToken cancellationToken = default)
 161    {
 4162        DateTimeOffset normalizedFromUtc = recordedFromUtc.ToUniversalTime();
 4163        DateTimeOffset normalizedToUtc = recordedToUtc.ToUniversalTime();
 164
 4165        if (normalizedFromUtc > normalizedToUtc)
 166        {
 2167            throw new ArgumentException(
 2168                "The recorded UTC range start must be less than or equal to the range end.",
 2169                nameof(recordedFromUtc));
 170        }
 171
 2172        List<AsiBackboneAuditLedgerRecordEntity> entities = await LedgerRecords()
 2173            .Where(record => record.RecordedUtc >= normalizedFromUtc && record.RecordedUtc <= normalizedToUtc)
 2174            .OrderBy(record => record.RecordedUtc)
 2175            .ThenBy(record => record.RecordId)
 2176            .ToListAsync(cancellationToken)
 2177            .ConfigureAwait(false);
 178
 2179        return ToRecords(entities);
 2180    }
 181
 182    private IQueryable<AsiBackboneAuditLedgerRecordEntity> LedgerRecords()
 183    {
 16184        return dbContext.Set<AsiBackboneAuditLedgerRecordEntity>().AsNoTracking();
 185    }
 186
 187    private static AsiBackboneAuditLedgerRecordEntity ToEntity(AuditLedgerRecord record)
 188    {
 14189        return new AsiBackboneAuditLedgerRecordEntity
 14190        {
 14191            RecordId = record.RecordId,
 14192            SchemaVersion = record.SchemaVersion,
 14193            EventId = record.EventId,
 14194            AuditResidueId = record.AuditResidueId,
 14195            OccurredUtc = record.OccurredUtc,
 14196            RecordedUtc = record.RecordedUtc,
 14197            ActorId = record.ActorId,
 14198            ActorType = record.ActorType,
 14199            ActorDisplayName = record.ActorDisplayName,
 14200            OperationName = record.OperationName,
 14201            Outcome = record.Outcome,
 14202            ReasonCodesJson = JsonSerializer.Serialize(record.ReasonCodes, JsonOptions),
 14203            CorrelationId = record.CorrelationId,
 14204            TraceId = record.TraceId,
 14205            SpanId = record.SpanId,
 14206            ParentSpanId = record.ParentSpanId,
 14207            DecisionLatencyMs = record.DecisionLatencyMs,
 14208            ConstraintSetHash = record.ConstraintSetHash,
 14209            ConstraintCount = record.ConstraintCount,
 14210            RiskScore = record.RiskScore,
 14211            PolicyScope = record.PolicyScope,
 14212            TenantHash = record.TenantHash,
 14213            OrganizationHash = record.OrganizationHash,
 14214            EmitterStatus = record.EmitterStatus,
 14215            EmitterProvider = record.EmitterProvider,
 14216            OutboxSequence = record.OutboxSequence,
 14217            GatewayExecutionId = record.GatewayExecutionId,
 14218            DecisionStage = record.DecisionStage,
 14219            PolicyVersion = record.PolicyVersion,
 14220            PolicyHash = record.PolicyHash,
 14221            HandshakeId = record.HandshakeId,
 14222            AcknowledgmentId = record.AcknowledgmentId,
 14223            CapabilityTokenId = record.CapabilityTokenId,
 14224            PreviousRecordHash = record.PreviousRecordHash,
 14225            RecordHash = record.RecordHash,
 14226            SignatureKeyId = record.SignatureKeyId,
 14227            SignatureAlgorithm = record.SignatureAlgorithm,
 14228            SignatureValue = record.SignatureValue,
 14229            MetadataJson = JsonSerializer.Serialize(record.Metadata, JsonOptions)
 14230        };
 231    }
 232
 233    private static AsiBackboneAuditLedgerReasonCodeEntity[] ToReasonCodeEntities(
 234        Guid auditLedgerRecordId,
 235        IReadOnlyList<string> reasonCodes)
 236    {
 14237        return [.. reasonCodes
 24238            .Select((reasonCode, index) => new AsiBackboneAuditLedgerReasonCodeEntity
 24239            {
 24240                AuditLedgerRecordId = auditLedgerRecordId,
 24241                Sequence = index,
 24242                ReasonCode = reasonCode
 24243            })];
 244    }
 245
 246    private static AsiBackboneAuditLedgerMetadataEntity[] ToMetadataEntities(
 247        Guid auditLedgerRecordId,
 248        IReadOnlyDictionary<string, string> metadata)
 249    {
 14250        return [.. metadata
 22251            .Select(item => new AsiBackboneAuditLedgerMetadataEntity
 22252            {
 22253                AuditLedgerRecordId = auditLedgerRecordId,
 22254                MetadataKey = item.Key,
 22255                MetadataValue = item.Value
 22256            })];
 257    }
 258
 259    private static AuditLedgerRecord[] ToRecords(IEnumerable<AsiBackboneAuditLedgerRecordEntity> entities)
 260    {
 8261        return [.. entities.Select(ToRecord)];
 262    }
 263
 264    private static AuditLedgerRecord ToRecord(AsiBackboneAuditLedgerRecordEntity entity)
 265    {
 20266        string[] reasonCodes = DeserializeReasonCodes(entity.ReasonCodesJson);
 20267        ReadOnlyDictionary<string, string> metadata = DeserializeMetadata(entity.MetadataJson);
 268
 20269        var residue = new EntityAuditResidue(
 20270            entity.EventId,
 20271            entity.AuditResidueId,
 20272            entity.SchemaVersion,
 20273            entity.OccurredUtc,
 20274            entity.ActorId,
 20275            entity.ActorType,
 20276            entity.ActorDisplayName,
 20277            entity.OperationName,
 20278            entity.Outcome,
 20279            Array.AsReadOnly(reasonCodes),
 20280            entity.CorrelationId,
 20281            entity.TraceId,
 20282            entity.SpanId,
 20283            entity.ParentSpanId,
 20284            entity.DecisionLatencyMs,
 20285            entity.ConstraintSetHash,
 20286            entity.ConstraintCount,
 20287            entity.RiskScore,
 20288            entity.PolicyScope,
 20289            entity.TenantHash,
 20290            entity.OrganizationHash,
 20291            entity.EmitterStatus,
 20292            entity.EmitterProvider,
 20293            entity.OutboxSequence,
 20294            entity.GatewayExecutionId,
 20295            entity.DecisionStage,
 20296            entity.PolicyVersion,
 20297            entity.PolicyHash,
 20298            metadata);
 299
 20300        return AuditLedgerRecord.FromResidue(
 20301            residue,
 20302            entity.RecordId,
 20303            entity.RecordedUtc,
 20304            entity.HandshakeId,
 20305            entity.AcknowledgmentId,
 20306            entity.CapabilityTokenId,
 20307            entity.PreviousRecordHash,
 20308            entity.RecordHash,
 20309            entity.SignatureKeyId,
 20310            entity.SignatureAlgorithm,
 20311            entity.SignatureValue,
 20312            schemaVersion: entity.SchemaVersion);
 313    }
 314
 315    private static string[] DeserializeReasonCodes(string? json)
 316    {
 20317        return string.IsNullOrWhiteSpace(json)
 20318            ? []
 20319            : JsonSerializer.Deserialize<string[]>(json, JsonOptions) ?? [];
 320    }
 321
 322    private static ReadOnlyDictionary<string, string> DeserializeMetadata(string? json)
 323    {
 20324        if (string.IsNullOrWhiteSpace(json))
 325        {
 2326            return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(StringComparer.Ordinal));
 327        }
 328
 18329        Dictionary<string, string>? metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions)
 330
 18331        return metadata is null || metadata.Count == 0
 18332            ? new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(StringComparer.Ordinal))
 18333            : new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(metadata, StringComparer.Ordinal));
 334    }
 335
 20336    private sealed class EntityAuditResidue(
 20337        string eventId,
 20338        string? auditResidueId,
 20339        string schemaVersion,
 20340        DateTimeOffset occurredUtc,
 20341        string actorId,
 20342        AsiBackboneActorType actorType,
 20343        string? actorDisplayName,
 20344        string operationName,
 20345        string outcome,
 20346        IReadOnlyList<string> reasonCodes,
 20347        string? correlationId,
 20348        string? traceId,
 20349        string? spanId,
 20350        string? parentSpanId,
 20351        long? decisionLatencyMs,
 20352        string? constraintSetHash,
 20353        int? constraintCount,
 20354        double? riskScore,
 20355        string? policyScope,
 20356        string? tenantHash,
 20357        string? organizationHash,
 20358        string? emitterStatus,
 20359        string? emitterProvider,
 20360        long? outboxSequence,
 20361        string? gatewayExecutionId,
 20362        string? decisionStage,
 20363        string? policyVersion,
 20364        string? policyHash,
 20365        IReadOnlyDictionary<string, string> metadata) : IAsiBackboneAuditResidue
 366    {
 40367        public string EventId { get; } = eventId;
 368
 40369        public string? AuditResidueId { get; } = auditResidueId;
 370
 20371        public string SchemaVersion { get; } = schemaVersion;
 372
 40373        public DateTimeOffset OccurredUtc { get; } = occurredUtc;
 374
 40375        public string ActorId { get; } = actorId;
 376
 40377        public AsiBackboneActorType ActorType { get; } = actorType;
 378
 40379        public string? ActorDisplayName { get; } = actorDisplayName;
 380
 40381        public string OperationName { get; } = operationName;
 382
 40383        public string Outcome { get; } = outcome;
 384
 40385        public IReadOnlyList<string> ReasonCodes { get; } = reasonCodes;
 386
 40387        public string? CorrelationId { get; } = correlationId;
 388
 40389        public string? TraceId { get; } = traceId;
 390
 40391        public string? SpanId { get; } = spanId;
 392
 40393        public string? ParentSpanId { get; } = parentSpanId;
 394
 40395        public long? DecisionLatencyMs { get; } = decisionLatencyMs;
 396
 40397        public string? ConstraintSetHash { get; } = constraintSetHash;
 398
 40399        public int? ConstraintCount { get; } = constraintCount;
 400
 40401        public double? RiskScore { get; } = riskScore;
 402
 40403        public string? PolicyScope { get; } = policyScope;
 404
 40405        public string? TenantHash { get; } = tenantHash;
 406
 40407        public string? OrganizationHash { get; } = organizationHash;
 408
 40409        public string? EmitterStatus { get; } = emitterStatus;
 410
 40411        public string? EmitterProvider { get; } = emitterProvider;
 412
 40413        public long? OutboxSequence { get; } = outboxSequence;
 414
 40415        public string? GatewayExecutionId { get; } = gatewayExecutionId;
 416
 40417        public string? DecisionStage { get; } = decisionStage;
 418
 40419        public string? PolicyVersion { get; } = policyVersion;
 420
 40421        public string? PolicyHash { get; } = policyHash;
 422
 40423        public IReadOnlyDictionary<string, string> Metadata { get; } = metadata;
 424    }
 425}

Methods/Properties

.cctor()
.ctor(Microsoft.EntityFrameworkCore.DbContext)
AppendAsync()
FindByRecordIdAsync()
FindByCorrelationIdAsync()
FindByTraceIdAsync()
FindByActorIdAsync()
FindByRecordedUtcRangeAsync()
LedgerRecords()
ToEntity(AsiBackbone.Core.Audit.AuditLedgerRecord)
ToReasonCodeEntities(System.Guid,System.Collections.Generic.IReadOnlyList`1<System.String>)
ToMetadataEntities(System.Guid,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
ToRecords(System.Collections.Generic.IEnumerable`1<AsiBackbone.EntityFrameworkCore.Persistence.AsiBackboneAuditLedgerRecordEntity>)
ToRecord(AsiBackbone.EntityFrameworkCore.Persistence.AsiBackboneAuditLedgerRecordEntity)
DeserializeReasonCodes(System.String)
DeserializeMetadata(System.String)
.ctor(System.String,System.String,System.String,System.DateTimeOffset,System.String,AsiBackbone.Core.Actors.AsiBackboneActorType,System.String,System.String,System.String,System.Collections.Generic.IReadOnlyList`1<System.String>,System.String,System.String,System.String,System.String,System.Nullable`1<System.Int64>,System.String,System.Nullable`1<System.Int32>,System.Nullable`1<System.Double>,System.String,System.String,System.String,System.String,System.String,System.Nullable`1<System.Int64>,System.String,System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
get_EventId()
get_AuditResidueId()
get_SchemaVersion()
get_OccurredUtc()
get_ActorId()
get_ActorType()
get_ActorDisplayName()
get_OperationName()
get_Outcome()
get_ReasonCodes()
get_CorrelationId()
get_TraceId()
get_SpanId()
get_ParentSpanId()
get_DecisionLatencyMs()
get_ConstraintSetHash()
get_ConstraintCount()
get_RiskScore()
get_PolicyScope()
get_TenantHash()
get_OrganizationHash()
get_EmitterStatus()
get_EmitterProvider()
get_OutboxSequence()
get_GatewayExecutionId()
get_DecisionStage()
get_PolicyVersion()
get_PolicyHash()
get_Metadata()