< 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
72%
Covered lines: 217
Uncovered lines: 81
Coverable lines: 298
Total lines: 504
Line coverage: 72.8%
Branch coverage
60%
Covered branches: 49
Total branches: 81
Branch coverage: 60.4%
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()94.44%191885.24%
GetHealthAsync()75%44100%
QueryAsync()0%7280%
AddEntry(...)100%11100%
ApplyResult(...)63.63%121180%
CalculateRetryDelay(...)100%11100%
ResolveDestination(...)50%22100%
NormalizeDestination(...)50%88100%
CreateIdentity(...)50%2285.71%
ValidateExisting(...)50%66100%
ToMessage(...)100%11100%
ToItem(...)100%210%
SanitizeError(...)66.66%66100%
UtcNow()100%11100%
get_Id()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
 1622    public ApplicationAuditCompletionOutbox(
 1623        ApplicationDbContext dbContext,
 1624        IOptions<ApplicationAuditCompletionOutboxOptions> options,
 1625        IEnumerable<IApplicationAuditCompletionPublisher> publishers,
 1626        TimeProvider timeProvider)
 27    {
 1628        ArgumentNullException.ThrowIfNull(dbContext);
 1629        ArgumentNullException.ThrowIfNull(options);
 1630        ArgumentNullException.ThrowIfNull(publishers);
 1631        ArgumentNullException.ThrowIfNull(timeProvider);
 32
 1633        _dbContext = dbContext;
 1634        _options = options.Value;
 1635        _timeProvider = timeProvider;
 36
 1637        var publisherMap = new Dictionary<string, IApplicationAuditCompletionPublisher>(StringComparer.OrdinalIgnoreCase
 4838        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
 1648        _publishers = publisherMap;
 1649    }
 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        {
 292            return null;
 93        }
 94
 1495        string resolvedDestination = ResolveDestination(destination);
 1496        OutboxIdentity identity = CreateIdentity(resolvedDestination, receipt.MutationBatchId);
 97
 1498        ApplicationAuditCompletionOutboxEntry? existing = dbContext
 1499            .ApplicationAuditCompletionOutboxEntries
 14100            .Local
 16101            .SingleOrDefault(entry => entry.Id == identity.Id);
 102
 14103        existing ??= await dbContext.ApplicationAuditCompletionOutboxEntries
 14104            .FindAsync([identity.Id], cancellationToken)
 14105            .ConfigureAwait(false);
 106
 14107        return existing is not null
 14108            ? ValidateExisting(existing, resolvedDestination, receipt.MutationBatchId, identity.IdempotencyKey)
 14109            : AddEntry(dbContext, receipt, resolvedDestination, identity);
 16110    }
 111
 112    /// <inheritdoc />
 113    public async Task<ApplicationAuditCompletionDispatchSummary> DispatchReadyAsync(
 114        CancellationToken cancellationToken = default)
 115    {
 14116        cancellationToken.ThrowIfCancellationRequested();
 12117        if (!_options.Enabled)
 118        {
 2119            return new(false, 0, 0, 0, 0, 0, 0);
 120        }
 121
 10122        DateTime now = UtcNow();
 10123        List<ApplicationAuditCompletionOutboxEntry> entries = await _dbContext
 10124            .ApplicationAuditCompletionOutboxEntries
 10125            .Where(entry =>
 10126                (entry.Status == ApplicationAuditCompletionOutboxStatuses.Pending ||
 10127                 entry.Status == ApplicationAuditCompletionOutboxStatuses.RetryableFailure ||
 10128                 entry.Status == ApplicationAuditCompletionOutboxStatuses.Deferred) &&
 10129                (!entry.NextAttemptUtc.HasValue || entry.NextAttemptUtc <= now))
 10130            .OrderBy(entry => entry.CreatedUtc)
 10131            .Take(_options.BatchSize)
 10132            .ToListAsync(cancellationToken)
 10133            .ConfigureAwait(false);
 134
 10135        int delivered = 0;
 10136        int retryScheduled = 0;
 10137        int failed = 0;
 10138        int deferred = 0;
 10139        int deadLettered = 0;
 140
 40141        foreach (ApplicationAuditCompletionOutboxEntry entry in entries)
 142        {
 10143            cancellationToken.ThrowIfCancellationRequested();
 144            ApplicationAuditCompletionPublishResult result;
 145
 10146            if (!_publishers.TryGetValue(entry.Destination, out IApplicationAuditCompletionPublisher? publisher))
 147            {
 4148                result = ApplicationAuditCompletionPublishResult.Defer(
 4149                    "PublisherUnavailable",
 4150                    "No audit-completion publisher is registered for the configured destination.",
 4151                    _options.DeferredRetryDelay);
 152            }
 153            else
 154            {
 155                try
 156                {
 6157                    result = await publisher
 6158                        .PublishAsync(ToMessage(entry), cancellationToken)
 6159                        .ConfigureAwait(false);
 6160                }
 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
 10173            cancellationToken.ThrowIfCancellationRequested();
 10174            ApplyResult(entry, result, now);
 10175            await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 176
 10177            switch (entry.Status)
 178            {
 179                case ApplicationAuditCompletionOutboxStatuses.Delivered:
 2180                    delivered++;
 2181                    break;
 182                case ApplicationAuditCompletionOutboxStatuses.RetryableFailure:
 2183                    retryScheduled++;
 2184                    break;
 185                case ApplicationAuditCompletionOutboxStatuses.Failed:
 0186                    failed++;
 0187                    break;
 188                case ApplicationAuditCompletionOutboxStatuses.Deferred:
 4189                    deferred++;
 4190                    break;
 191                case ApplicationAuditCompletionOutboxStatuses.DeadLettered:
 2192                    deadLettered++;
 193                    break;
 194                default:
 195                    break;
 196            }
 10197        }
 198
 10199        return new(
 10200            true,
 10201            entries.Count,
 10202            delivered,
 10203            retryScheduled,
 10204            failed,
 10205            deferred,
 10206            deadLettered);
 12207    }
 208
 209    /// <inheritdoc />
 210    public async Task<ApplicationAuditCompletionOutboxHealth> GetHealthAsync(
 211        CancellationToken cancellationToken = default)
 212    {
 4213        cancellationToken.ThrowIfCancellationRequested();
 4214        if (!_options.Enabled)
 215        {
 2216            return new(false, 0, null, 0, 0);
 217        }
 218
 2219        IQueryable<ApplicationAuditCompletionOutboxEntry> backlog = _dbContext
 2220            .ApplicationAuditCompletionOutboxEntries
 2221            .AsNoTracking()
 2222            .Where(entry =>
 2223                entry.Status == ApplicationAuditCompletionOutboxStatuses.Pending ||
 2224                entry.Status == ApplicationAuditCompletionOutboxStatuses.RetryableFailure ||
 2225                entry.Status == ApplicationAuditCompletionOutboxStatuses.Deferred);
 226
 2227        long backlogCount = await backlog.LongCountAsync(cancellationToken).ConfigureAwait(false);
 2228        DateTime? oldestCreatedUtc = await backlog
 2229            .MinAsync(entry => (DateTime?)entry.CreatedUtc, cancellationToken)
 2230            .ConfigureAwait(false);
 2231        long retryCount = await _dbContext.ApplicationAuditCompletionOutboxEntries
 2232            .AsNoTracking()
 2233            .SumAsync(entry => (long)entry.RetryCount, cancellationToken)
 2234            .ConfigureAwait(false);
 2235        long deadLetterCount = await _dbContext.ApplicationAuditCompletionOutboxEntries
 2236            .AsNoTracking()
 2237            .LongCountAsync(
 2238                entry => entry.Status == ApplicationAuditCompletionOutboxStatuses.DeadLettered,
 2239                cancellationToken)
 2240            .ConfigureAwait(false);
 241
 2242        TimeSpan? oldestAge = oldestCreatedUtc.HasValue
 2243            ? UtcNow() - oldestCreatedUtc.Value
 2244            : null;
 245
 2246        return new(true, backlogCount, oldestAge, retryCount, deadLetterCount);
 4247    }
 248
 249    /// <inheritdoc />
 250    public async Task<IReadOnlyList<ApplicationAuditCompletionOutboxItem>> QueryAsync(
 251        ApplicationAuditCompletionOutboxQueryRequest request,
 252        CancellationToken cancellationToken = default)
 253    {
 0254        ArgumentNullException.ThrowIfNull(request);
 0255        cancellationToken.ThrowIfCancellationRequested();
 256
 0257        if (!_options.Enabled)
 258        {
 0259            return [];
 260        }
 261
 0262        int maximumResults = Math.Clamp(request.MaximumResults, 1, 500);
 0263        IQueryable<ApplicationAuditCompletionOutboxEntry> query = _dbContext
 0264            .ApplicationAuditCompletionOutboxEntries
 0265            .AsNoTracking();
 266
 0267        if (!string.IsNullOrWhiteSpace(request.Status))
 268        {
 0269            string status = request.Status.Trim();
 0270            query = query.Where(entry => entry.Status == status);
 271        }
 272
 0273        if (!string.IsNullOrWhiteSpace(request.Destination))
 274        {
 0275            string destination = request.Destination.Trim();
 0276            query = query.Where(entry => entry.Destination == destination);
 277        }
 278
 0279        if (!string.IsNullOrWhiteSpace(request.MutationBatchId))
 280        {
 0281            string mutationBatchId = request.MutationBatchId.Trim();
 0282            query = query.Where(entry => entry.MutationBatchId == mutationBatchId);
 283        }
 284
 0285        List<ApplicationAuditCompletionOutboxEntry> entries = await query
 0286            .OrderByDescending(entry => entry.CreatedUtc)
 0287            .Take(maximumResults)
 0288            .ToListAsync(cancellationToken)
 0289            .ConfigureAwait(false);
 290
 0291        return entries.Select(ToItem).ToArray();
 0292    }
 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    {
 10331        entry.LastAttemptUtc = attemptedUtc;
 10332        entry.DeliveredUtc = null;
 10333        entry.LastErrorCode = SanitizeError(result.ErrorCode, 128);
 10334        entry.LastErrorMessage = SanitizeError(result.ErrorMessage, _options.MaxErrorDetailLength);
 10335        entry.ConcurrencyStamp = DataEntity.NewConcurrencyStamp();
 336
 10337        switch (result.Disposition)
 338        {
 339            case ApplicationAuditCompletionPublishDisposition.Delivered:
 2340                entry.Status = ApplicationAuditCompletionOutboxStatuses.Delivered;
 2341                entry.DeliveredUtc = attemptedUtc;
 2342                entry.NextAttemptUtc = null;
 2343                entry.LastErrorCode = null;
 2344                entry.LastErrorMessage = null;
 2345                break;
 346            case ApplicationAuditCompletionPublishDisposition.RetryableFailure:
 4347                entry.RetryCount++;
 4348                if (entry.RetryCount >= _options.MaxRetryAttempts)
 349                {
 2350                    entry.Status = ApplicationAuditCompletionOutboxStatuses.DeadLettered;
 2351                    entry.NextAttemptUtc = null;
 352                }
 353                else
 354                {
 2355                    entry.Status = ApplicationAuditCompletionOutboxStatuses.RetryableFailure;
 2356                    entry.NextAttemptUtc = attemptedUtc +
 2357                        (result.RetryAfter ?? CalculateRetryDelay(entry.RetryCount));
 358                }
 359
 2360                break;
 361            case ApplicationAuditCompletionPublishDisposition.Failed:
 0362                entry.RetryCount++;
 0363                entry.Status = ApplicationAuditCompletionOutboxStatuses.Failed;
 0364                entry.NextAttemptUtc = null;
 0365                break;
 366            case ApplicationAuditCompletionPublishDisposition.Deferred:
 4367                entry.Status = ApplicationAuditCompletionOutboxStatuses.Deferred;
 4368                entry.NextAttemptUtc = attemptedUtc +
 4369                    (result.RetryAfter ?? _options.DeferredRetryDelay);
 4370                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    {
 2379        int exponent = Math.Clamp(retryCount - 1, 0, 20);
 2380        double ticks = Math.Min(
 2381            _options.MaxRetryDelay.Ticks,
 2382            _options.BaseRetryDelay.Ticks * Math.Pow(2, exponent));
 2383        return TimeSpan.FromTicks((long)ticks);
 384    }
 385
 386    private string ResolveDestination(string? destination)
 387    {
 14388        return NormalizeDestination(
 14389            string.IsNullOrWhiteSpace(destination)
 14390                ? _options.DefaultDestination
 14391                : destination);
 392    }
 393
 394    private static string NormalizeDestination(string? destination)
 395    {
 22396        string normalized = destination?.Trim() ?? string.Empty;
 22397        return string.IsNullOrWhiteSpace(normalized)
 22398            ? throw new InvalidOperationException("An audit-completion destination must be configured.")
 22399            : normalized.Length > 128
 22400            ? throw new InvalidOperationException("An audit-completion destination cannot exceed 128 characters.")
 22401            : normalized;
 402    }
 403
 404    private static OutboxIdentity CreateIdentity(string destination, string mutationBatchId)
 405    {
 14406        if (string.IsNullOrWhiteSpace(mutationBatchId))
 407        {
 0408            throw new InvalidOperationException("An audit-completion receipt must contain a mutation batch identifier.")
 409        }
 410
 14411        byte[] input = Encoding.UTF8.GetBytes($"{destination}\n{mutationBatchId.Trim()}");
 14412        byte[] hash = SHA256.HashData(input);
 14413        return new OutboxIdentity(
 14414            new Guid(hash.AsSpan(0, 16)),
 14415            $"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    {
 2424        return !existing.Destination.Equals(destination, StringComparison.OrdinalIgnoreCase) ||
 2425            !existing.MutationBatchId.Equals(mutationBatchId, StringComparison.Ordinal) ||
 2426            !existing.IdempotencyKey.Equals(idempotencyKey, StringComparison.Ordinal)
 2427            ? throw new InvalidOperationException(
 2428                "The deterministic audit-completion outbox identity resolved to conflicting persisted data.")
 2429            : existing;
 430    }
 431
 432    private static ApplicationAuditCompletionMessage ToMessage(
 433        ApplicationAuditCompletionOutboxEntry entry)
 434    {
 6435        return new(
 6436            entry.SchemaVersion,
 6437            entry.Destination,
 6438            entry.IdempotencyKey,
 6439            entry.MutationBatchId,
 6440            entry.AuditRecordCount,
 6441            entry.PersistenceOutcome,
 6442            entry.ReceiptCompletedUtc,
 6443            entry.MutationManifestHash,
 6444            entry.MutationManifestAlgorithm,
 6445            entry.MutationManifestSchemaVersion,
 6446            entry.OperationExecutionId,
 6447            entry.ExecutionAttemptId,
 6448            entry.DecisionAuditRecordId,
 6449            entry.CorrelationId,
 6450            entry.TraceId);
 451    }
 452
 453    private static ApplicationAuditCompletionOutboxItem ToItem(
 454        ApplicationAuditCompletionOutboxEntry entry)
 455    {
 0456        return new(
 0457            entry.Id,
 0458            entry.SchemaVersion,
 0459            entry.Destination,
 0460            entry.IdempotencyKey,
 0461            entry.MutationBatchId,
 0462            entry.AuditRecordCount,
 0463            entry.PersistenceOutcome,
 0464            entry.ReceiptCompletedUtc,
 0465            entry.MutationManifestHash,
 0466            entry.MutationManifestAlgorithm,
 0467            entry.MutationManifestSchemaVersion,
 0468            entry.OperationExecutionId,
 0469            entry.ExecutionAttemptId,
 0470            entry.DecisionAuditRecordId,
 0471            entry.CorrelationId,
 0472            entry.TraceId,
 0473            entry.Status,
 0474            entry.RetryCount,
 0475            entry.CreatedUtc,
 0476            entry.LastAttemptUtc,
 0477            entry.NextAttemptUtc,
 0478            entry.DeliveredUtc,
 0479            entry.LastErrorCode,
 0480            entry.LastErrorMessage);
 481    }
 482
 483    private static string? SanitizeError(string? value, int maximumLength)
 484    {
 20485        if (string.IsNullOrWhiteSpace(value))
 486        {
 8487            return null;
 488        }
 489
 12490        string sanitized = new([.. value
 12491            .Trim()
 428492            .Select(character => char.IsControl(character) ? ' ' : character)]);
 12493        return sanitized.Length <= maximumLength
 12494            ? sanitized
 12495            : sanitized[..maximumLength];
 496    }
 497
 498    private DateTime UtcNow()
 499    {
 24500        return _timeProvider.GetUtcNow().UtcDateTime;
 501    }
 502
 54503    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()
get_Id()