< Summary

Information
Class: ProjectTemplate.Infrastructure.Data.Auditing.ApplicationAuditCompletionOutbox
Assembly: ProjectTemplate.Infrastructure
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/ApplicationAuditCompletionOutbox.cs
Line coverage
89%
Covered lines: 266
Uncovered lines: 31
Coverable lines: 297
Total lines: 504
Line coverage: 89.5%
Branch coverage
74%
Covered branches: 57
Total branches: 77
Branch coverage: 74%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)75%4490%
Stage(...)0%4260%
StageAsync()100%66100%
DispatchReadyAsync()93.75%171685.24%
GetHealthAsync()75%44100%
QueryAsync()100%88100%
AddEntry(...)100%11100%
ApplyResult(...)63.63%121180%
CalculateRetryDelay(...)100%11100%
ResolveDestination(...)100%22100%
NormalizeDestination(...)62.5%88100%
CreateIdentity(...)100%22100%
ValidateExisting(...)50%66100%
ToMessage(...)100%11100%
ToItem(...)100%11100%
SanitizeError(...)75%44100%
UtcNow()100%11100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/ApplicationAuditCompletionOutbox.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using Microsoft.EntityFrameworkCore;
 4using Microsoft.Extensions.Options;
 5using ProjectTemplate.Infrastructure.Data.Entities;
 6
 7namespace ProjectTemplate.Infrastructure.Data.Auditing;
 8
 9/// <summary>
 10/// Implements durable audit-completion staging, dispatch, and minimized operational queries.
 11/// </summary>
 12public sealed class ApplicationAuditCompletionOutbox :
 13    IApplicationAuditCompletionOutbox,
 14    IApplicationAuditCompletionOutboxDispatcher,
 15    IApplicationAuditCompletionOutboxQuery
 16{
 17    private readonly ApplicationDbContext _dbContext;
 18    private readonly ApplicationAuditCompletionOutboxOptions _options;
 19    private readonly Dictionary<string, IApplicationAuditCompletionPublisher> _publishers;
 20    private readonly TimeProvider _timeProvider;
 21
 1722    public ApplicationAuditCompletionOutbox(
 1723        ApplicationDbContext dbContext,
 1724        IOptions<ApplicationAuditCompletionOutboxOptions> options,
 1725        IEnumerable<IApplicationAuditCompletionPublisher> publishers,
 1726        TimeProvider timeProvider)
 27    {
 1728        ArgumentNullException.ThrowIfNull(dbContext);
 1729        ArgumentNullException.ThrowIfNull(options);
 1730        ArgumentNullException.ThrowIfNull(publishers);
 1731        ArgumentNullException.ThrowIfNull(timeProvider);
 32
 1733        _dbContext = dbContext;
 1734        _options = options.Value;
 1735        _timeProvider = timeProvider;
 36
 1737        var publisherMap = new Dictionary<string, IApplicationAuditCompletionPublisher>(StringComparer.OrdinalIgnoreCase
 5038        foreach (IApplicationAuditCompletionPublisher publisher in publishers)
 39        {
 840            string destination = NormalizeDestination(publisher.Destination);
 841            if (!publisherMap.TryAdd(destination, publisher))
 42            {
 043                throw new InvalidOperationException(
 044                    $"More than one audit-completion publisher is registered for destination '{destination}'.");
 45            }
 46        }
 47
 1748        _publishers = publisherMap;
 1749    }
 50
 51    /// <inheritdoc />
 52    public ApplicationAuditCompletionOutboxEntry? Stage(
 53        ApplicationDbContext dbContext,
 54        ApplicationMutationAuditReceipt receipt,
 55        string? destination = null)
 56    {
 057        ArgumentNullException.ThrowIfNull(dbContext);
 058        ArgumentNullException.ThrowIfNull(receipt);
 59
 060        if (!_options.Enabled)
 61        {
 062            return null;
 63        }
 64
 065        string resolvedDestination = ResolveDestination(destination);
 066        OutboxIdentity identity = CreateIdentity(resolvedDestination, receipt.MutationBatchId);
 67
 068        ApplicationAuditCompletionOutboxEntry? existing = dbContext
 069            .ApplicationAuditCompletionOutboxEntries
 070            .Local
 071            .SingleOrDefault(entry => entry.Id == identity.Id)
 072            ?? dbContext.ApplicationAuditCompletionOutboxEntries.Find(identity.Id);
 73
 074        return existing is not null
 075            ? ValidateExisting(existing, resolvedDestination, receipt.MutationBatchId, identity.IdempotencyKey)
 076            : AddEntry(dbContext, receipt, resolvedDestination, identity);
 77    }
 78
 79    /// <inheritdoc />
 80    public async ValueTask<ApplicationAuditCompletionOutboxEntry?> StageAsync(
 81        ApplicationDbContext dbContext,
 82        ApplicationMutationAuditReceipt receipt,
 83        string? destination = null,
 84        CancellationToken cancellationToken = default)
 85    {
 1686        ArgumentNullException.ThrowIfNull(dbContext);
 1687        ArgumentNullException.ThrowIfNull(receipt);
 1688        cancellationToken.ThrowIfCancellationRequested();
 89
 1690        if (!_options.Enabled)
 91        {
 192            return null;
 93        }
 94
 1595        string resolvedDestination = ResolveDestination(destination);
 1496        OutboxIdentity identity = CreateIdentity(resolvedDestination, receipt.MutationBatchId);
 97
 1398        ApplicationAuditCompletionOutboxEntry? existing = dbContext
 1399            .ApplicationAuditCompletionOutboxEntries
 13100            .Local
 13101            .SingleOrDefault(entry => entry.Id == identity.Id);
 102
 13103        existing ??= await dbContext.ApplicationAuditCompletionOutboxEntries
 13104            .FindAsync([identity.Id], cancellationToken)
 13105            .ConfigureAwait(false);
 106
 13107        return existing is not null
 13108            ? ValidateExisting(existing, resolvedDestination, receipt.MutationBatchId, identity.IdempotencyKey)
 13109            : AddEntry(dbContext, receipt, resolvedDestination, identity);
 14110    }
 111
 112    /// <inheritdoc />
 113    public async Task<ApplicationAuditCompletionDispatchSummary> DispatchReadyAsync(
 114        CancellationToken cancellationToken = default)
 115    {
 11116        cancellationToken.ThrowIfCancellationRequested();
 10117        if (!_options.Enabled)
 118        {
 1119            return new(false, 0, 0, 0, 0, 0, 0);
 120        }
 121
 9122        DateTime now = UtcNow();
 9123        List<ApplicationAuditCompletionOutboxEntry> entries = await _dbContext
 9124            .ApplicationAuditCompletionOutboxEntries
 9125            .Where(entry =>
 9126                (entry.Status == ApplicationAuditCompletionOutboxStatuses.Pending ||
 9127                 entry.Status == ApplicationAuditCompletionOutboxStatuses.RetryableFailure ||
 9128                 entry.Status == ApplicationAuditCompletionOutboxStatuses.Deferred) &&
 9129                (!entry.NextAttemptUtc.HasValue || entry.NextAttemptUtc <= now))
 9130            .OrderBy(entry => entry.CreatedUtc)
 9131            .Take(_options.BatchSize)
 9132            .ToListAsync(cancellationToken)
 9133            .ConfigureAwait(false);
 134
 9135        int delivered = 0;
 9136        int retryScheduled = 0;
 9137        int failed = 0;
 9138        int deferred = 0;
 9139        int deadLettered = 0;
 140
 36141        foreach (ApplicationAuditCompletionOutboxEntry entry in entries)
 142        {
 9143            cancellationToken.ThrowIfCancellationRequested();
 144            ApplicationAuditCompletionPublishResult result;
 145
 9146            if (!_publishers.TryGetValue(entry.Destination, out IApplicationAuditCompletionPublisher? publisher))
 147            {
 2148                result = ApplicationAuditCompletionPublishResult.Defer(
 2149                    "PublisherUnavailable",
 2150                    "No audit-completion publisher is registered for the configured destination.",
 2151                    _options.DeferredRetryDelay);
 152            }
 153            else
 154            {
 155                try
 156                {
 7157                    result = await publisher
 7158                        .PublishAsync(ToMessage(entry), cancellationToken)
 7159                        .ConfigureAwait(false);
 7160                }
 0161                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 162                {
 0163                    throw;
 164                }
 0165                catch (Exception exception)
 166                {
 0167                    result = ApplicationAuditCompletionPublishResult.Retry(
 0168                        exception.GetType().Name,
 0169                        exception.Message);
 0170                }
 171            }
 172
 9173            cancellationToken.ThrowIfCancellationRequested();
 9174            ApplyResult(entry, result, now);
 9175            await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 176
 9177            switch (entry.Status)
 178            {
 179                case ApplicationAuditCompletionOutboxStatuses.Delivered:
 5180                    delivered++;
 5181                    break;
 182                case ApplicationAuditCompletionOutboxStatuses.RetryableFailure:
 1183                    retryScheduled++;
 1184                    break;
 185                case ApplicationAuditCompletionOutboxStatuses.Failed:
 0186                    failed++;
 0187                    break;
 188                case ApplicationAuditCompletionOutboxStatuses.Deferred:
 2189                    deferred++;
 2190                    break;
 191                case ApplicationAuditCompletionOutboxStatuses.DeadLettered:
 1192                    deadLettered++;
 193                    break;
 194                default:
 195                    break;
 196            }
 9197        }
 198
 9199        return new(
 9200            true,
 9201            entries.Count,
 9202            delivered,
 9203            retryScheduled,
 9204            failed,
 9205            deferred,
 9206            deadLettered);
 10207    }
 208
 209    /// <inheritdoc />
 210    public async Task<ApplicationAuditCompletionOutboxHealth> GetHealthAsync(
 211        CancellationToken cancellationToken = default)
 212    {
 2213        cancellationToken.ThrowIfCancellationRequested();
 2214        if (!_options.Enabled)
 215        {
 1216            return new(false, 0, null, 0, 0);
 217        }
 218
 1219        IQueryable<ApplicationAuditCompletionOutboxEntry> backlog = _dbContext
 1220            .ApplicationAuditCompletionOutboxEntries
 1221            .AsNoTracking()
 1222            .Where(entry =>
 1223                entry.Status == ApplicationAuditCompletionOutboxStatuses.Pending ||
 1224                entry.Status == ApplicationAuditCompletionOutboxStatuses.RetryableFailure ||
 1225                entry.Status == ApplicationAuditCompletionOutboxStatuses.Deferred);
 226
 1227        long backlogCount = await backlog.LongCountAsync(cancellationToken).ConfigureAwait(false);
 1228        DateTime? oldestCreatedUtc = await backlog
 1229            .MinAsync(entry => (DateTime?)entry.CreatedUtc, cancellationToken)
 1230            .ConfigureAwait(false);
 1231        long retryCount = await _dbContext.ApplicationAuditCompletionOutboxEntries
 1232            .AsNoTracking()
 1233            .SumAsync(entry => (long)entry.RetryCount, cancellationToken)
 1234            .ConfigureAwait(false);
 1235        long deadLetterCount = await _dbContext.ApplicationAuditCompletionOutboxEntries
 1236            .AsNoTracking()
 1237            .LongCountAsync(
 1238                entry => entry.Status == ApplicationAuditCompletionOutboxStatuses.DeadLettered,
 1239                cancellationToken)
 1240            .ConfigureAwait(false);
 241
 1242        TimeSpan? oldestAge = oldestCreatedUtc.HasValue
 1243            ? UtcNow() - oldestCreatedUtc.Value
 1244            : null;
 245
 1246        return new(true, backlogCount, oldestAge, retryCount, deadLetterCount);
 2247    }
 248
 249    /// <inheritdoc />
 250    public async Task<IReadOnlyList<ApplicationAuditCompletionOutboxItem>> QueryAsync(
 251        ApplicationAuditCompletionOutboxQueryRequest request,
 252        CancellationToken cancellationToken = default)
 253    {
 2254        ArgumentNullException.ThrowIfNull(request);
 2255        cancellationToken.ThrowIfCancellationRequested();
 256
 2257        if (!_options.Enabled)
 258        {
 1259            return [];
 260        }
 261
 1262        int maximumResults = Math.Clamp(request.MaximumResults, 1, 500);
 1263        IQueryable<ApplicationAuditCompletionOutboxEntry> query = _dbContext
 1264            .ApplicationAuditCompletionOutboxEntries
 1265            .AsNoTracking();
 266
 1267        if (!string.IsNullOrWhiteSpace(request.Status))
 268        {
 1269            string status = request.Status.Trim();
 1270            query = query.Where(entry => entry.Status == status);
 271        }
 272
 1273        if (!string.IsNullOrWhiteSpace(request.Destination))
 274        {
 1275            string destination = request.Destination.Trim();
 1276            query = query.Where(entry => entry.Destination == destination);
 277        }
 278
 1279        if (!string.IsNullOrWhiteSpace(request.MutationBatchId))
 280        {
 1281            string mutationBatchId = request.MutationBatchId.Trim();
 1282            query = query.Where(entry => entry.MutationBatchId == mutationBatchId);
 283        }
 284
 1285        List<ApplicationAuditCompletionOutboxEntry> entries = await query
 1286            .OrderByDescending(entry => entry.CreatedUtc)
 1287            .Take(maximumResults)
 1288            .ToListAsync(cancellationToken)
 1289            .ConfigureAwait(false);
 290
 1291        return entries.Select(ToItem).ToArray();
 2292    }
 293
 294    private ApplicationAuditCompletionOutboxEntry AddEntry(
 295        ApplicationDbContext dbContext,
 296        ApplicationMutationAuditReceipt receipt,
 297        string destination,
 298        OutboxIdentity identity)
 299    {
 12300        var entry = new ApplicationAuditCompletionOutboxEntry
 12301        {
 12302            Id = identity.Id,
 12303            SchemaVersion = ApplicationAuditCompletionOutboxEntry.CurrentSchemaVersion,
 12304            Destination = destination,
 12305            IdempotencyKey = identity.IdempotencyKey,
 12306            MutationBatchId = receipt.MutationBatchId,
 12307            AuditRecordCount = receipt.AuditRecordCount,
 12308            PersistenceOutcome = receipt.PersistenceOutcome,
 12309            ReceiptCompletedUtc = receipt.CompletedUtc.UtcDateTime,
 12310            MutationManifestHash = receipt.MutationManifestHash,
 12311            MutationManifestAlgorithm = receipt.MutationManifestAlgorithm,
 12312            MutationManifestSchemaVersion = receipt.MutationManifestSchemaVersion,
 12313            OperationExecutionId = receipt.OperationExecutionId,
 12314            ExecutionAttemptId = receipt.ExecutionAttemptId,
 12315            DecisionAuditRecordId = receipt.DecisionAuditRecordId,
 12316            CorrelationId = receipt.CorrelationId,
 12317            TraceId = receipt.TraceId,
 12318            Status = ApplicationAuditCompletionOutboxStatuses.Pending,
 12319            CreatedUtc = UtcNow()
 12320        };
 321
 12322        dbContext.ApplicationAuditCompletionOutboxEntries.Add(entry);
 12323        return entry;
 324    }
 325
 326    private void ApplyResult(
 327        ApplicationAuditCompletionOutboxEntry entry,
 328        ApplicationAuditCompletionPublishResult result,
 329        DateTime attemptedUtc)
 330    {
 9331        entry.LastAttemptUtc = attemptedUtc;
 9332        entry.DeliveredUtc = null;
 9333        entry.LastErrorCode = SanitizeError(result.ErrorCode, 128);
 9334        entry.LastErrorMessage = SanitizeError(result.ErrorMessage, _options.MaxErrorDetailLength);
 9335        entry.ConcurrencyStamp = DataEntity.NewConcurrencyStamp();
 336
 9337        switch (result.Disposition)
 338        {
 339            case ApplicationAuditCompletionPublishDisposition.Delivered:
 5340                entry.Status = ApplicationAuditCompletionOutboxStatuses.Delivered;
 5341                entry.DeliveredUtc = attemptedUtc;
 5342                entry.NextAttemptUtc = null;
 5343                entry.LastErrorCode = null;
 5344                entry.LastErrorMessage = null;
 5345                break;
 346            case ApplicationAuditCompletionPublishDisposition.RetryableFailure:
 2347                entry.RetryCount++;
 2348                if (entry.RetryCount >= _options.MaxRetryAttempts)
 349                {
 1350                    entry.Status = ApplicationAuditCompletionOutboxStatuses.DeadLettered;
 1351                    entry.NextAttemptUtc = null;
 352                }
 353                else
 354                {
 1355                    entry.Status = ApplicationAuditCompletionOutboxStatuses.RetryableFailure;
 1356                    entry.NextAttemptUtc = attemptedUtc +
 1357                        (result.RetryAfter ?? CalculateRetryDelay(entry.RetryCount));
 358                }
 359
 1360                break;
 361            case ApplicationAuditCompletionPublishDisposition.Failed:
 0362                entry.RetryCount++;
 0363                entry.Status = ApplicationAuditCompletionOutboxStatuses.Failed;
 0364                entry.NextAttemptUtc = null;
 0365                break;
 366            case ApplicationAuditCompletionPublishDisposition.Deferred:
 2367                entry.Status = ApplicationAuditCompletionOutboxStatuses.Deferred;
 2368                entry.NextAttemptUtc = attemptedUtc +
 2369                    (result.RetryAfter ?? _options.DeferredRetryDelay);
 2370                break;
 371            default:
 0372                throw new InvalidOperationException(
 0373                    $"Unsupported audit-completion publish disposition '{result.Disposition}'.");
 374        }
 375    }
 376
 377    private TimeSpan CalculateRetryDelay(int retryCount)
 378    {
 1379        int exponent = Math.Clamp(retryCount - 1, 0, 20);
 1380        double ticks = Math.Min(
 1381            _options.MaxRetryDelay.Ticks,
 1382            _options.BaseRetryDelay.Ticks * Math.Pow(2, exponent));
 1383        return TimeSpan.FromTicks((long)ticks);
 384    }
 385
 386    private string ResolveDestination(string? destination)
 387    {
 15388        return NormalizeDestination(
 15389            string.IsNullOrWhiteSpace(destination)
 15390                ? _options.DefaultDestination
 15391                : destination);
 392    }
 393
 394    private static string NormalizeDestination(string? destination)
 395    {
 23396        string normalized = destination?.Trim() ?? string.Empty;
 23397        return string.IsNullOrWhiteSpace(normalized)
 23398            ? throw new InvalidOperationException("An audit-completion destination must be configured.")
 23399            : normalized.Length > 128
 23400            ? throw new InvalidOperationException("An audit-completion destination cannot exceed 128 characters.")
 23401            : normalized;
 402    }
 403
 404    private static OutboxIdentity CreateIdentity(string destination, string mutationBatchId)
 405    {
 14406        if (string.IsNullOrWhiteSpace(mutationBatchId))
 407        {
 1408            throw new InvalidOperationException("An audit-completion receipt must contain a mutation batch identifier.")
 409        }
 410
 13411        byte[] input = Encoding.UTF8.GetBytes($"{destination}\n{mutationBatchId.Trim()}");
 13412        byte[] hash = SHA256.HashData(input);
 13413        return new OutboxIdentity(
 13414            new Guid(hash.AsSpan(0, 16)),
 13415            $"ncat-audit-completion:{Convert.ToHexString(hash)}");
 416    }
 417
 418    private static ApplicationAuditCompletionOutboxEntry ValidateExisting(
 419        ApplicationAuditCompletionOutboxEntry existing,
 420        string destination,
 421        string mutationBatchId,
 422        string idempotencyKey)
 423    {
 1424        return !existing.Destination.Equals(destination, StringComparison.OrdinalIgnoreCase) ||
 1425            !existing.MutationBatchId.Equals(mutationBatchId, StringComparison.Ordinal) ||
 1426            !existing.IdempotencyKey.Equals(idempotencyKey, StringComparison.Ordinal)
 1427            ? throw new InvalidOperationException(
 1428                "The deterministic audit-completion outbox identity resolved to conflicting persisted data.")
 1429            : existing;
 430    }
 431
 432    private static ApplicationAuditCompletionMessage ToMessage(
 433        ApplicationAuditCompletionOutboxEntry entry)
 434    {
 7435        return new(
 7436            entry.SchemaVersion,
 7437            entry.Destination,
 7438            entry.IdempotencyKey,
 7439            entry.MutationBatchId,
 7440            entry.AuditRecordCount,
 7441            entry.PersistenceOutcome,
 7442            entry.ReceiptCompletedUtc,
 7443            entry.MutationManifestHash,
 7444            entry.MutationManifestAlgorithm,
 7445            entry.MutationManifestSchemaVersion,
 7446            entry.OperationExecutionId,
 7447            entry.ExecutionAttemptId,
 7448            entry.DecisionAuditRecordId,
 7449            entry.CorrelationId,
 7450            entry.TraceId);
 451    }
 452
 453    private static ApplicationAuditCompletionOutboxItem ToItem(
 454        ApplicationAuditCompletionOutboxEntry entry)
 455    {
 1456        return new(
 1457            entry.Id,
 1458            entry.SchemaVersion,
 1459            entry.Destination,
 1460            entry.IdempotencyKey,
 1461            entry.MutationBatchId,
 1462            entry.AuditRecordCount,
 1463            entry.PersistenceOutcome,
 1464            entry.ReceiptCompletedUtc,
 1465            entry.MutationManifestHash,
 1466            entry.MutationManifestAlgorithm,
 1467            entry.MutationManifestSchemaVersion,
 1468            entry.OperationExecutionId,
 1469            entry.ExecutionAttemptId,
 1470            entry.DecisionAuditRecordId,
 1471            entry.CorrelationId,
 1472            entry.TraceId,
 1473            entry.Status,
 1474            entry.RetryCount,
 1475            entry.CreatedUtc,
 1476            entry.LastAttemptUtc,
 1477            entry.NextAttemptUtc,
 1478            entry.DeliveredUtc,
 1479            entry.LastErrorCode,
 1480            entry.LastErrorMessage);
 481    }
 482
 483    private static string? SanitizeError(string? value, int maximumLength)
 484    {
 18485        if (string.IsNullOrWhiteSpace(value))
 486        {
 12487            return null;
 488        }
 489
 6490        string sanitized = new([.. value
 6491            .Trim()
 6492            .Select(character => char.IsControl(character) ? ' ' : character)]);
 6493        return sanitized.Length <= maximumLength
 6494            ? sanitized
 6495            : sanitized[..maximumLength];
 496    }
 497
 498    private DateTime UtcNow()
 499    {
 22500        return _timeProvider.GetUtcNow().UtcDateTime;
 501    }
 502
 503    private sealed record OutboxIdentity(Guid Id, string IdempotencyKey);
 504}

Methods/Properties

.ctor(ProjectTemplate.Infrastructure.Data.ApplicationDbContext,Microsoft.Extensions.Options.IOptions`1<ProjectTemplate.Infrastructure.Data.Auditing.ApplicationAuditCompletionOutboxOptions>,System.Collections.Generic.IEnumerable`1<ProjectTemplate.Infrastructure.Data.Auditing.IApplicationAuditCompletionPublisher>,System.TimeProvider)
Stage(ProjectTemplate.Infrastructure.Data.ApplicationDbContext,ProjectTemplate.Infrastructure.Data.Auditing.ApplicationMutationAuditReceipt,System.String)
StageAsync()
DispatchReadyAsync()
GetHealthAsync()
QueryAsync()
AddEntry(ProjectTemplate.Infrastructure.Data.ApplicationDbContext,ProjectTemplate.Infrastructure.Data.Auditing.ApplicationMutationAuditReceipt,System.String,ProjectTemplate.Infrastructure.Data.Auditing.ApplicationAuditCompletionOutbox/OutboxIdentity)
ApplyResult(ProjectTemplate.Infrastructure.Data.Entities.ApplicationAuditCompletionOutboxEntry,ProjectTemplate.Infrastructure.Data.Auditing.ApplicationAuditCompletionPublishResult,System.DateTime)
CalculateRetryDelay(System.Int32)
ResolveDestination(System.String)
NormalizeDestination(System.String)
CreateIdentity(System.String,System.String)
ValidateExisting(ProjectTemplate.Infrastructure.Data.Entities.ApplicationAuditCompletionOutboxEntry,System.String,System.String,System.String)
ToMessage(ProjectTemplate.Infrastructure.Data.Entities.ApplicationAuditCompletionOutboxEntry)
ToItem(ProjectTemplate.Infrastructure.Data.Entities.ApplicationAuditCompletionOutboxEntry)
SanitizeError(System.String,System.Int32)
UtcNow()