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

Information
Class: AsyncResponse.PayloadRecoveryClassifier
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/PayloadRecoveryClassifier.cs
Line coverage
100%
Covered lines: 25
Uncovered lines: 0
Coverable lines: 25
Total lines: 102
Line coverage: 100%
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
.cctor()100%11100%
ShouldResume(...)91.67%1212100%
ResolvePayloadType(...)100%88100%

File(s)

/home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/PayloadRecoveryClassifier.cs

#LineLine coverage
 1using System.Collections.Concurrent;
 2using System.Diagnostics.CodeAnalysis;
 3using System.Text.Json;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// Resolves the lost-subscriber recovery route for a response payload by asking it
 9/// <see cref="IAsyncResponsePayload.ShouldResumeOnRecovery"/>.
 10/// <para>
 11/// Payloads arriving through a broker ingress are untyped (a raw <see cref="JsonElement"/> / JSON
 12/// string), so the payload type the original waiter registered for (persisted in the recovery
 13/// state) is used to materialize the payload before asking it.
 14/// </para>
 15/// </summary>
 16internal static class PayloadRecoveryClassifier
 17{
 318    private static readonly ConcurrentDictionary<string, Type> PayloadTypes = new(StringComparer.Ordinal);
 19
 20    /// <summary>
 21    /// Attempts to decide whether <paramref name="payload"/> should resume the flow on the
 22    /// lost-subscriber path.
 23    /// </summary>
 24    /// <param name="payload">
 25    /// The payload as received by <c>SetResponse</c>: either an already-typed
 26    /// <see cref="IAsyncResponsePayload"/>, or raw JSON (<see cref="JsonElement"/> / JSON string)
 27    /// when the response came through a broker ingress.
 28    /// </param>
 29    /// <param name="payloadTypeFullName">
 30    /// Full name of the payload type the waiter subscribed for, from the recovery state.
 31    /// </param>
 32    /// <returns>
 33    /// <c>true</c> to resume, <c>false</c> to fail, or <c>null</c> when the payload cannot be
 34    /// classified — a <c>null</c> payload, missing/unresolvable type information, or a conversion
 35    /// failure. Callers must treat <c>null</c> conservatively as "do not resume", so a payload that
 36    /// cannot be understood never takes the happy path.
 37    /// </returns>
 38    public static bool? ShouldResume(object? payload, string? payloadTypeFullName)
 39    {
 40        try
 41        {
 42            // Typed payloads (published directly by in-process services) answer for themselves.
 343            if (payload is IAsyncResponsePayload typedPayload)
 44            {
 345                return typedPayload.ShouldResumeOnRecovery();
 46            }
 47
 348            if (payload is null || string.IsNullOrWhiteSpace(payloadTypeFullName))
 49            {
 350                return null;
 51            }
 52
 253            var payloadType = ResolvePayloadType(payloadTypeFullName!);
 354            if (payloadType is null || !typeof(IAsyncResponsePayload).IsAssignableFrom(payloadType))
 55            {
 356                return null;
 57            }
 58
 359            return payload.ConvertTo(payloadType) is IAsyncResponsePayload materialized
 360                ? materialized.ShouldResumeOnRecovery()
 361                : null;
 62        }
 363        catch
 64        {
 65            // A payload that cannot be materialized as the registered type carries no usable domain
 66            // state; treat it conservatively as "do not resume". The failure callback invocation
 67            // performs the same conversion and surfaces the error through the existing path.
 368            return null;
 69        }
 370    }
 71
 72    [UnconditionalSuppressMessage("Trimming", "IL2026",
 73        Justification = "The persisted payload type name comes from a recoverable-waiter registration whose payload type
 74                        "parameter is statically referenced by the registering app; an unresolvable name is answered wit
 75                        "null — the conservative 'do not resume' route — plus a type-resolution-failure diagnostic.")]
 76    private static Type? ResolvePayloadType(string payloadTypeFullName)
 77    {
 378        if (PayloadTypes.TryGetValue(payloadTypeFullName, out var cached))
 379            return cached;
 80
 381        var resolved = AppDomain.CurrentDomain
 382            .GetAssemblies()
 383            .Select(a => a.GetType(payloadTypeFullName, throwOnError: false))
 384            .FirstOrDefault(t => t != null);
 85
 86        // Opt-in fallback for payload types loaded into a non-default AssemblyLoadContext (plugins).
 387        resolved ??= AsyncResponseTypeResolution.Resolve(payloadTypeFullName);
 88
 289        if (resolved is not null)
 90        {
 391            PayloadTypes.TryAdd(payloadTypeFullName, resolved);
 92        }
 93        else
 94        {
 95            // Surface the silent "couldn't materialize the payload type" path so operators can
 96            // correlate a recovery that routed to failure with a missing/ALC-loaded type.
 397            AsyncResponseDiagnostics.RecordTypeResolutionFailure("payload");
 98        }
 99
 2100        return resolved;
 101    }
 102}