< Summary

Information
Class: AsiBackbone.Core.Outbox.GovernanceOutboxEntry
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Outbox/GovernanceOutboxEntry.cs
Line coverage
100%
Covered lines: 166
Uncovered lines: 0
Coverable lines: 166
Total lines: 390
Line coverage: 100%
Branch coverage
95%
Covered branches: 61
Total branches: 64
Branch coverage: 95.3%
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%88100%
get_OutboxEntryId()100%11100%
get_Envelope()100%11100%
get_Status()100%11100%
get_CreatedUtc()100%11100%
get_UpdatedUtc()100%11100%
get_RetryCount()100%11100%
get_MaxRetryCount()100%11100%
get_NextRetryUtc()100%11100%
get_LastError()100%11100%
get_ProviderName()100%11100%
get_ProviderRecordId()100%11100%
get_DeadLetterReason()100%11100%
get_Metadata()100%11100%
get_IsDelivered()100%11100%
get_IsDeadLettered()100%11100%
get_HasMetadata()100%11100%
Create(...)100%22100%
Restore(...)100%11100%
IsRetryReady(...)94.44%1818100%
MarkDelivered(...)100%22100%
MarkFailed(...)100%88100%
MarkDeferred(...)100%22100%
MarkDeadLettered(...)100%22100%
Copy(...)100%22100%
NormalizeIdentifier(...)100%22100%
NormalizeOptional(...)100%22100%
MergeMetadata(...)100%11100%
NormalizeMetadata(...)87.5%1616100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Outbox/GovernanceOutboxEntry.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using AsiBackbone.Core.Emissions;
 3
 4namespace AsiBackbone.Core.Outbox;
 5
 6/// <summary>
 7/// Represents a provider-neutral durable outbox entry for a governance emission envelope.
 8/// </summary>
 9/// <remarks>
 10/// Outbox entries are intended to be persisted before optional downstream provider delivery is attempted.
 11/// </remarks>
 12public sealed class GovernanceOutboxEntry
 13{
 14    private const int DefaultMaxRetryCount = 5;
 15
 516    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 517        new ReadOnlyDictionary<string, string>(
 518            new Dictionary<string, string>(StringComparer.Ordinal));
 19
 27420    private GovernanceOutboxEntry(
 27421        string outboxEntryId,
 27422        GovernanceEmissionEnvelope envelope,
 27423        GovernanceEmissionStatus status,
 27424        DateTimeOffset createdUtc,
 27425        DateTimeOffset updatedUtc,
 27426        int retryCount,
 27427        int maxRetryCount,
 27428        DateTimeOffset? nextRetryUtc,
 27429        GovernanceEmissionError? lastError,
 27430        string? providerName,
 27431        string? providerRecordId,
 27432        string? deadLetterReason,
 27433        IReadOnlyDictionary<string, string> metadata)
 34    {
 27435        ArgumentException.ThrowIfNullOrWhiteSpace(outboxEntryId);
 27336        ArgumentNullException.ThrowIfNull(envelope);
 37
 27338        if (!Enum.IsDefined(status))
 39        {
 140            throw new ArgumentOutOfRangeException(nameof(status), status, "Outbox status must be defined.");
 41        }
 42
 27243        if (retryCount < 0)
 44        {
 145            throw new ArgumentOutOfRangeException(nameof(retryCount), retryCount, "Retry count must be greater than or e
 46        }
 47
 27148        if (maxRetryCount < 0)
 49        {
 250            throw new ArgumentOutOfRangeException(nameof(maxRetryCount), maxRetryCount, "Maximum retry count must be gre
 51        }
 52
 26953        OutboxEntryId = outboxEntryId.Trim();
 26954        Envelope = envelope;
 26955        Status = status;
 26956        CreatedUtc = createdUtc.ToUniversalTime();
 26957        UpdatedUtc = updatedUtc.ToUniversalTime();
 26958        RetryCount = retryCount;
 26959        MaxRetryCount = maxRetryCount;
 26960        NextRetryUtc = nextRetryUtc?.ToUniversalTime();
 26961        LastError = lastError;
 26962        ProviderName = NormalizeOptional(providerName);
 26963        ProviderRecordId = NormalizeOptional(providerRecordId);
 26964        DeadLetterReason = NormalizeOptional(deadLetterReason);
 26965        Metadata = metadata;
 26966    }
 67
 68    /// <summary>
 69    /// Gets the stable outbox entry identifier.
 70    /// </summary>
 42371    public string OutboxEntryId { get; }
 72
 73    /// <summary>
 74    /// Gets the provider-neutral governance emission envelope being persisted for delivery.
 75    /// </summary>
 27576    public GovernanceEmissionEnvelope Envelope { get; }
 77
 78    /// <summary>
 79    /// Gets the current provider-neutral outbox status.
 80    /// </summary>
 41381    public GovernanceEmissionStatus Status { get; }
 82
 83    /// <summary>
 84    /// Gets the UTC timestamp when the outbox entry was created.
 85    /// </summary>
 20486    public DateTimeOffset CreatedUtc { get; }
 87
 88    /// <summary>
 89    /// Gets the UTC timestamp when the outbox entry was last updated.
 90    /// </summary>
 9491    public DateTimeOffset UpdatedUtc { get; }
 92
 93    /// <summary>
 94    /// Gets the number of failed or deferred delivery attempts recorded for this entry.
 95    /// </summary>
 20096    public int RetryCount { get; }
 97
 98    /// <summary>
 99    /// Gets the maximum retry count before the entry should transition to dead-lettered.
 100    /// </summary>
 209101    public int MaxRetryCount { get; }
 102
 103    /// <summary>
 104    /// Gets the next UTC retry timestamp, when retry scheduling is active.
 105    /// </summary>
 175106    public DateTimeOffset? NextRetryUtc { get; }
 107
 108    /// <summary>
 109    /// Gets the last provider-neutral emission error, when available.
 110    /// </summary>
 111111    public GovernanceEmissionError? LastError { get; }
 112
 113    /// <summary>
 114    /// Gets the provider name associated with the most recent attempt, when available.
 115    /// </summary>
 97116    public string? ProviderName { get; }
 117
 118    /// <summary>
 119    /// Gets the provider-side record identifier, when delivery returned one and it is safe to store.
 120    /// </summary>
 91121    public string? ProviderRecordId { get; }
 122
 123    /// <summary>
 124    /// Gets the dead-letter reason, when the entry has reached a terminal dead-letter state.
 125    /// </summary>
 93126    public string? DeadLetterReason { get; }
 127
 128    /// <summary>
 129    /// Gets minimized provider-neutral outbox metadata.
 130    /// </summary>
 160131    public IReadOnlyDictionary<string, string> Metadata { get; }
 132
 133    /// <summary>
 134    /// Gets a value indicating whether this entry was delivered successfully.
 135    /// </summary>
 64136    public bool IsDelivered => Status is GovernanceEmissionStatus.Delivered;
 137
 138    /// <summary>
 139    /// Gets a value indicating whether this entry has reached a terminal dead-letter state.
 140    /// </summary>
 57141    public bool IsDeadLettered => Status is GovernanceEmissionStatus.DeadLettered;
 142
 143    /// <summary>
 144    /// Gets a value indicating whether metadata is present.
 145    /// </summary>
 5146    public bool HasMetadata => Metadata.Count > 0;
 147
 148    /// <summary>
 149    /// Creates a pending durable governance outbox entry.
 150    /// </summary>
 151    public static GovernanceOutboxEntry Create(
 152        GovernanceEmissionEnvelope envelope,
 153        string? outboxEntryId = null,
 154        DateTimeOffset? createdUtc = null,
 155        int maxRetryCount = DefaultMaxRetryCount,
 156        IReadOnlyDictionary<string, string>? metadata = null)
 157    {
 101158        DateTimeOffset timestamp = createdUtc ?? DateTimeOffset.UtcNow;
 159
 101160        return new GovernanceOutboxEntry(
 101161            NormalizeIdentifier(outboxEntryId),
 101162            envelope,
 101163            GovernanceEmissionStatus.Pending,
 101164            timestamp,
 101165            timestamp,
 101166            retryCount: 0,
 101167            maxRetryCount,
 101168            nextRetryUtc: null,
 101169            lastError: null,
 101170            providerName: null,
 101171            providerRecordId: null,
 101172            deadLetterReason: null,
 101173            NormalizeMetadata(metadata));
 174    }
 175
 176    /// <summary>
 177    /// Restores a durable outbox entry from provider-neutral storage.
 178    /// </summary>
 179    /// <remarks>
 180    /// This factory exists for storage adapters. It does not perform provider emission and does not add any provider de
 181    /// </remarks>
 182    public static GovernanceOutboxEntry Restore(
 183        GovernanceEmissionEnvelope envelope,
 184        GovernanceEmissionStatus status,
 185        string outboxEntryId,
 186        DateTimeOffset createdUtc,
 187        DateTimeOffset updatedUtc,
 188        int retryCount = 0,
 189        int maxRetryCount = DefaultMaxRetryCount,
 190        DateTimeOffset? nextRetryUtc = null,
 191        GovernanceEmissionError? lastError = null,
 192        string? providerName = null,
 193        string? providerRecordId = null,
 194        string? deadLetterReason = null,
 195        IReadOnlyDictionary<string, string>? metadata = null)
 196    {
 112197        return new GovernanceOutboxEntry(
 112198            outboxEntryId,
 112199            envelope,
 112200            status,
 112201            createdUtc,
 112202            updatedUtc,
 112203            retryCount,
 112204            maxRetryCount,
 112205            nextRetryUtc,
 112206            lastError,
 112207            providerName,
 112208            providerRecordId,
 112209            deadLetterReason,
 112210            NormalizeMetadata(metadata));
 211    }
 212
 213    /// <summary>
 214    /// Determines whether the entry is ready for retry at the supplied UTC timestamp.
 215    /// </summary>
 216    public bool IsRetryReady(DateTimeOffset utcNow)
 217    {
 42218        return !IsDelivered && !IsDeadLettered && RetryCount < MaxRetryCount && Status is GovernanceEmissionStatus.Defer
 219    }
 220
 221    /// <summary>
 222    /// Returns a delivered copy of this entry.
 223    /// </summary>
 224    public GovernanceOutboxEntry MarkDelivered(
 225        GovernanceEmissionResult result,
 226        DateTimeOffset? updatedUtc = null)
 227    {
 20228        ArgumentNullException.ThrowIfNull(result);
 229
 20230        return !result.IsSuccess
 20231            ? throw new ArgumentException("Delivered outbox transitions require a successful emission result.", nameof(r
 20232            : Copy(
 20233            GovernanceEmissionStatus.Delivered,
 20234            updatedUtc,
 20235            retryCount: RetryCount,
 20236            nextRetryUtc: null,
 20237            lastError: null,
 20238            providerName: result.ProviderName,
 20239            providerRecordId: result.ProviderRecordId,
 20240            deadLetterReason: null,
 20241            metadata: MergeMetadata(Metadata, result.Metadata));
 242    }
 243
 244    /// <summary>
 245    /// Returns a failed or retryable-failure copy of this entry.
 246    /// </summary>
 247    public GovernanceOutboxEntry MarkFailed(
 248        GovernanceEmissionError governanceEmissionError,
 249        DateTimeOffset? nextRetryUtc = null,
 250        DateTimeOffset? updatedUtc = null)
 251    {
 27252        ArgumentNullException.ThrowIfNull(governanceEmissionError);
 253
 27254        int nextRetryCount = RetryCount + 1;
 27255        GovernanceEmissionStatus nextStatus = nextRetryCount >= MaxRetryCount
 27256            ? GovernanceEmissionStatus.DeadLettered
 27257            : governanceEmissionError.IsRetryable
 27258                ? GovernanceEmissionStatus.RetryableFailure
 27259                : GovernanceEmissionStatus.Failed;
 260
 27261        return Copy(
 27262            nextStatus,
 27263            updatedUtc,
 27264            nextRetryCount,
 27265            nextStatus is GovernanceEmissionStatus.DeadLettered ? null : nextRetryUtc,
 27266            governanceEmissionError,
 27267            governanceEmissionError.ProviderName,
 27268            providerRecordId: null,
 27269            nextStatus is GovernanceEmissionStatus.DeadLettered ? governanceEmissionError.Message : null,
 27270            Metadata);
 271    }
 272
 273    /// <summary>
 274    /// Returns a deferred copy of this entry.
 275    /// </summary>
 276    public GovernanceOutboxEntry MarkDeferred(
 277        GovernanceEmissionError? governanceEmissionError = null,
 278        DateTimeOffset? nextRetryUtc = null,
 279        DateTimeOffset? updatedUtc = null)
 280    {
 9281        return Copy(
 9282            GovernanceEmissionStatus.Deferred,
 9283            updatedUtc,
 9284            retryCount: RetryCount,
 9285            nextRetryUtc,
 9286            governanceEmissionError,
 9287            governanceEmissionError?.ProviderName,
 9288            providerRecordId: null,
 9289            deadLetterReason: null,
 9290            metadata: Metadata);
 291    }
 292
 293    /// <summary>
 294    /// Returns a dead-lettered copy of this entry.
 295    /// </summary>
 296    public GovernanceOutboxEntry MarkDeadLettered(
 297        GovernanceEmissionError governanceEmissionError,
 298        string? deadLetterReason = null,
 299        DateTimeOffset? updatedUtc = null)
 300    {
 6301        ArgumentNullException.ThrowIfNull(governanceEmissionError);
 302
 6303        return Copy(
 6304            GovernanceEmissionStatus.DeadLettered,
 6305            updatedUtc,
 6306            retryCount: RetryCount,
 6307            nextRetryUtc: null,
 6308            lastError: governanceEmissionError,
 6309            providerName: governanceEmissionError.ProviderName,
 6310            providerRecordId: null,
 6311            deadLetterReason: deadLetterReason ?? governanceEmissionError.Message,
 6312            metadata: Metadata);
 313    }
 314
 315    private GovernanceOutboxEntry Copy(
 316        GovernanceEmissionStatus status,
 317        DateTimeOffset? updatedUtc,
 318        int retryCount,
 319        DateTimeOffset? nextRetryUtc,
 320        GovernanceEmissionError? lastError,
 321        string? providerName,
 322        string? providerRecordId,
 323        string? deadLetterReason,
 324        IReadOnlyDictionary<string, string> metadata)
 325    {
 61326        return new GovernanceOutboxEntry(
 61327            OutboxEntryId,
 61328            Envelope,
 61329            status,
 61330            CreatedUtc,
 61331            updatedUtc ?? DateTimeOffset.UtcNow,
 61332            retryCount,
 61333            MaxRetryCount,
 61334            nextRetryUtc,
 61335            lastError,
 61336            providerName,
 61337            providerRecordId,
 61338            deadLetterReason,
 61339            metadata);
 340    }
 341
 342    private static string NormalizeIdentifier(string? identifier)
 343    {
 101344        return string.IsNullOrWhiteSpace(identifier)
 101345            ? Guid.NewGuid().ToString("N")
 101346            : identifier.Trim();
 347    }
 348
 349    private static string? NormalizeOptional(string? value)
 350    {
 807351        return string.IsNullOrWhiteSpace(value)
 807352            ? null
 807353            : value.Trim();
 354    }
 355
 356    private static IReadOnlyDictionary<string, string> MergeMetadata(
 357        IReadOnlyDictionary<string, string> originalMetadata,
 358        IReadOnlyDictionary<string, string> resultMetadata)
 359    {
 19360        return NormalizeMetadata(originalMetadata, resultMetadata);
 361    }
 362
 363    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 364        params IReadOnlyDictionary<string, string>?[] metadataSets)
 365    {
 232366        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 367
 966368        foreach (IReadOnlyDictionary<string, string>? metadata in metadataSets)
 369        {
 251370            if (metadata is null || metadata.Count == 0)
 371            {
 372                continue;
 373            }
 374
 92375            foreach (KeyValuePair<string, string> item in metadata)
 376            {
 29377                if (string.IsNullOrWhiteSpace(item.Key))
 378                {
 379                    continue;
 380                }
 381
 27382                normalizedMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 383            }
 384        }
 385
 232386        return normalizedMetadata.Count == 0
 232387            ? EmptyMetadata
 232388            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 389    }
 390}

Methods/Properties

.cctor()
.ctor(System.String,AsiBackbone.Core.Emissions.GovernanceEmissionEnvelope,AsiBackbone.Core.Emissions.GovernanceEmissionStatus,System.DateTimeOffset,System.DateTimeOffset,System.Int32,System.Int32,System.Nullable`1<System.DateTimeOffset>,AsiBackbone.Core.Emissions.GovernanceEmissionError,System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
get_OutboxEntryId()
get_Envelope()
get_Status()
get_CreatedUtc()
get_UpdatedUtc()
get_RetryCount()
get_MaxRetryCount()
get_NextRetryUtc()
get_LastError()
get_ProviderName()
get_ProviderRecordId()
get_DeadLetterReason()
get_Metadata()
get_IsDelivered()
get_IsDeadLettered()
get_HasMetadata()
Create(AsiBackbone.Core.Emissions.GovernanceEmissionEnvelope,System.String,System.Nullable`1<System.DateTimeOffset>,System.Int32,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
Restore(AsiBackbone.Core.Emissions.GovernanceEmissionEnvelope,AsiBackbone.Core.Emissions.GovernanceEmissionStatus,System.String,System.DateTimeOffset,System.DateTimeOffset,System.Int32,System.Int32,System.Nullable`1<System.DateTimeOffset>,AsiBackbone.Core.Emissions.GovernanceEmissionError,System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
IsRetryReady(System.DateTimeOffset)
MarkDelivered(AsiBackbone.Core.Emissions.GovernanceEmissionResult,System.Nullable`1<System.DateTimeOffset>)
MarkFailed(AsiBackbone.Core.Emissions.GovernanceEmissionError,System.Nullable`1<System.DateTimeOffset>,System.Nullable`1<System.DateTimeOffset>)
MarkDeferred(AsiBackbone.Core.Emissions.GovernanceEmissionError,System.Nullable`1<System.DateTimeOffset>,System.Nullable`1<System.DateTimeOffset>)
MarkDeadLettered(AsiBackbone.Core.Emissions.GovernanceEmissionError,System.String,System.Nullable`1<System.DateTimeOffset>)
Copy(AsiBackbone.Core.Emissions.GovernanceEmissionStatus,System.Nullable`1<System.DateTimeOffset>,System.Int32,System.Nullable`1<System.DateTimeOffset>,AsiBackbone.Core.Emissions.GovernanceEmissionError,System.String,System.String,System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
NormalizeIdentifier(System.String)
NormalizeOptional(System.String)
MergeMetadata(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
NormalizeMetadata(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>[])