< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.CorrelationIdGuard
Assembly: AsyncResponse.Core
File(s): /_/src/AsyncResponse.Core/CorrelationIdGuard.cs
Line coverage
96%
Covered lines: 26
Uncovered lines: 1
Coverable lines: 27
Total lines: 123
Line coverage: 96.2%
Branch coverage
95%
Covered branches: 19
Total branches: 20
Branch coverage: 95%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ThrowIfUnusable(...)100%44100%
IsUnroutable(...)100%44100%
IsUnpublishable(...)91.66%121292.3%
get_ErrorType()100%11100%

File(s)

/_/src/AsyncResponse.Core/CorrelationIdGuard.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using System.Diagnostics;
 3using System.Diagnostics.CodeAnalysis;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// The one place the portable correlation-id contract
 9/// (<see cref="AsyncResponseChannelOptions.CorrelationIdNotPortable"/>) is applied to a public
 10/// string, so that every channel enforces the same rule at the same boundary. An id that violates
 11/// it is not a cosmetic problem: it is truncated or rejected at its first relational write, and a
 12/// space-padded one is the SAME key as its trimmed form to a database while the library compares
 13/// ids ordinally — a response stored under it can surface at another waiter.
 14/// <para>
 15/// Two failures, told apart because callers can do different things about them. A BLANK id carries
 16/// no information to act on, so it is logged and skipped wherever it appears — that has always been
 17/// the behaviour and an inbound message with no id can only be dropped. A NON-BLANK id that breaks
 18/// the contract is a caller bug: the library throws it back at every public entry point, because
 19/// swallowing it turns a typo into a waiter that simply times out much later, with nothing at the
 20/// call site to explain why. Only the untrusted edge — <see cref="IAsyncResponseIngress"/> and the
 21/// raw publish path it drives — downgrades that to a drop, since a broker message that throws comes
 22/// straight back around on redelivery, forever.
 23/// </para>
 24/// </summary>
 25internal static class CorrelationIdGuard
 26{
 27    /// <summary>
 28    /// Validates an id the application supplied for a WAIT, before any subscription or
 29    /// recovery-state side effect exists to leak.
 30    /// </summary>
 31    internal static void ThrowIfUnusable([NotNull] string? correlationId)
 32    {
 709633        if (string.IsNullOrWhiteSpace(correlationId))
 1634            throw new ArgumentNullException(nameof(correlationId), "CorrelationId must not be empty or whitespace.");
 35
 708036        if (AsyncResponseChannelOptions.CorrelationIdNotPortable(correlationId) is { } rejection)
 4837            throw new ArgumentException(rejection, nameof(correlationId));
 703238    }
 39
 40    /// <summary>
 41    /// Classifies an id without acting on it: the shared answer behind every caller below, and
 42    /// behind the ingress's own drop decision.
 43    /// </summary>
 44    internal static bool IsUnroutable([NotNullWhen(false)] string? correlationId, out UnroutableReason reason)
 45    {
 702446        if (string.IsNullOrWhiteSpace(correlationId))
 47        {
 5848            reason = new("correlation_id_null", "no correlation id", ContractViolation: false);
 5849            return true;
 50        }
 51
 696652        if (AsyncResponseChannelOptions.CorrelationIdNotPortable(correlationId) is { } rejection)
 53        {
 4954            reason = new("correlation_id_not_portable", rejection, ContractViolation: true);
 4955            return true;
 56        }
 57
 691758        reason = default;
 691759        return false;
 60    }
 61
 62    /// <summary>
 63    /// Guards a PUBLISH. Returns <c>false</c> — narrowing <paramref name="correlationId"/> to
 64    /// non-null — when the publish may proceed, and <c>true</c> when it must be abandoned, having
 65    /// already logged the reason and marked <paramref name="activity"/>. A non-blank id that breaks
 66    /// the contract throws instead, unless <paramref name="dropContractViolations"/> says this
 67    /// caller is the untrusted edge.
 68    /// </summary>
 69    /// <param name="correlationId">The id the publish was addressed to.</param>
 70    /// <param name="logger">The publishing channel's logger.</param>
 71    /// <param name="activity">The publish activity, marked with the failure when there is one.</param>
 72    /// <param name="what">What is being dropped, for the log message: "the response", "the exception", …</param>
 73    /// <param name="dropped">
 74    /// The exception a <c>SetException</c> publish was carrying; included in the log so the
 75    /// technical failure it described is not lost along with its unroutable id.
 76    /// </param>
 77    /// <param name="dropContractViolations">
 78    /// <c>true</c> only on the raw publish path, which exists to serve inbound broker messages:
 79    /// throwing there would fail the delivery, and the transport would redeliver an id that can
 80    /// never become valid. The ingress rejects such ids before this point; this is the backstop.
 81    /// </param>
 82    internal static bool IsUnpublishable(
 83        [NotNullWhen(false)] string? correlationId,
 84        ILogger logger,
 85        Activity? activity,
 86        string what,
 87        Exception? dropped = null,
 88        bool dropContractViolations = false)
 89    {
 689590        if (!IsUnroutable(correlationId, out var reason))
 680891            return false;
 92
 8793        if (reason.ContractViolation && !dropContractViolations)
 2894            throw new ArgumentException(reason.Description, nameof(correlationId));
 95
 5996        if (reason.ContractViolation)
 97        {
 98            // Error, not warning: unlike a missing id this one looks routable, so left alone it
 99            // would fail much later — at the storage write, or worse, at somebody else's waiter.
 7100            if (dropped is null)
 7101                logger.LogError("Cannot publish {What}; the correlation id is outside the portable contract. {Rejection}
 102            else
 0103                logger.LogError("Cannot publish {What}; the correlation id is outside the portable contract. {Rejection}
 104        }
 52105        else if (dropped is null)
 106        {
 35107            logger.LogWarning("CorrelationId is null; cannot publish {What}.", what);
 108        }
 109        else
 110        {
 17111            logger.LogWarning("CorrelationId is null; cannot publish {What}. Exception: {ExceptionMessage}", what, dropp
 112        }
 113
 59114        AsyncResponseDiagnostics.SetError(activity, reason.ErrorType, $"Cannot publish {what}: {reason.Description}.");
 59115        return true;
 116    }
 117
 118    /// <summary>
 119    /// Why an id cannot route: the span's <c>error.type</c> tag, the human-readable phrase, and
 120    /// whether it is a caller's contract violation (throwable) rather than a missing id.
 121    /// </summary>
 359122    internal readonly record struct UnroutableReason(string ErrorType, string Description, bool ContractViolation);
 123}