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

Information
Class: AsyncResponse.AsyncResponseEnvelope<T>
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseEnvelope.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 224
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%

File(s)

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

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Serialization;
 3using System.Text.Json.Serialization.Metadata;
 4
 5namespace AsyncResponse;
 6
 7/// <summary>
 8/// The transport envelope wrapping every published response: either a payload
 9/// (<see cref="Success"/> = true) or a technical failure description.
 10/// </summary>
 11/// <typeparam name="T">The payload type.</typeparam>
 12internal sealed class AsyncResponseEnvelope<T>
 13{
 14    /// <summary>
 15    /// Wire schema version, stamped with <see cref="AsyncResponseEnvelopeSchema.Current"/> when the
 16    /// envelope is created. The property is required on the wire; a waiter rejects a missing or
 17    /// unrecognized version rather than risk misreading it.
 18    /// </summary>
 319    public int SchemaVersion { get; set; } = AsyncResponseEnvelopeSchema.Current;
 20    public bool Success { get; set; }
 21    public T? Payload { get; set; }
 22    public string? ExceptionMessage { get; set; }
 23    public string? ExceptionStackTrace { get; set; }
 24}
 25
 26/// <summary>
 27/// Wire-schema version stamp for <see cref="AsyncResponseEnvelope{T}"/>. New envelopes are stamped
 28/// with <see cref="Current"/>; a waiter rejects any unrecognized version so a publisher cannot feed
 29/// an incompatible shape to a waiter.
 30/// </summary>
 31internal static class AsyncResponseEnvelopeSchema
 32{
 33    /// <summary>The current wire schema version written by this build.</summary>
 34    public const int Current = 1;
 35
 36    /// <summary>Returns <c>true</c> when an envelope with <paramref name="entryVersion"/> is safe to read on this build
 37    public static bool IsReadable(int entryVersion) => entryVersion == Current;
 38}
 39
 40/// <summary>
 41/// Pre-configured <see cref="JsonSerializerOptions"/> for (de)serializing
 42/// <see cref="AsyncResponseEnvelope{T}"/> instances with null-payload tolerance.
 43/// <para>
 44/// The envelope's metadata is provided converter-backed via
 45/// <see cref="JsonMetadataServices.CreateValueInfo{T}"/> (statically instantiated per
 46/// <typeparamref name="T"/>, no reflection), and everything else — including the payload type —
 47/// resolves through <see cref="AsyncResponseJson.Resolver"/>. Callers needing trim/AOT-clean
 48/// (de)serialization go through <see cref="AsyncResponseEnvelopeJson"/> rather than the
 49/// reflection-based <see cref="JsonSerializer"/> overloads.
 50/// </para>
 51/// </summary>
 52internal static class AsyncResponseEnvelopeOptions<T>
 53{
 54    public static readonly JsonSerializerOptions Instance = new() { TypeInfoResolver = new EnvelopeResolver() };
 55
 56    private sealed class EnvelopeResolver : IJsonTypeInfoResolver
 57    {
 58        public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
 59            => type == typeof(AsyncResponseEnvelope<T>)
 60                ? JsonMetadataServices.CreateValueInfo<AsyncResponseEnvelope<T>>(options, new AsyncResponseEnvelopeConve
 61                : AsyncResponseJson.Resolver.GetTypeInfo(type, options);
 62    }
 63}
 64
 65/// <summary>
 66/// Trim/AOT-clean (de)serialization entry points for <see cref="AsyncResponseEnvelope{T}"/> —
 67/// the typed-metadata counterparts of <c>JsonSerializer.Serialize(envelope,
 68/// AsyncResponseEnvelopeOptions&lt;T&gt;.Instance)</c> and
 69/// <c>JsonSafety.SafeDeserialize&lt;AsyncResponseEnvelope&lt;T&gt;&gt;(json, …)</c>.
 70/// </summary>
 71internal static class AsyncResponseEnvelopeJson
 72{
 73    /// <summary>Typed metadata for <see cref="AsyncResponseEnvelope{T}"/> bound to its pre-configured options.</summary
 74    public static JsonTypeInfo<AsyncResponseEnvelope<T>> TypeInfo<T>()
 75        => AsyncResponseJson.GetTypeInfo<AsyncResponseEnvelope<T>>(AsyncResponseEnvelopeOptions<T>.Instance);
 76
 77    /// <summary>Serializes an envelope for publishing.</summary>
 78    public static string Serialize<T>(AsyncResponseEnvelope<T> envelope)
 79        => JsonSerializer.Serialize(envelope, TypeInfo<T>());
 80
 81    /// <summary>Deserializes an envelope with the standard broker-ingress guards (see <see cref="JsonSafety"/>).</summa
 82    public static AsyncResponseEnvelope<T>? SafeDeserialize<T>(string json)
 83        => JsonSafety.SafeDeserialize(json, TypeInfo<T>());
 84}
 85
 86/// <summary>
 87/// Custom converter that tolerates a JSON <c>null</c> payload even when <typeparamref name="T"/>
 88/// is a non-nullable value type, assigning <c>default(T)</c> instead of throwing.
 89/// </summary>
 90internal sealed class AsyncResponseEnvelopeConverter<T> : JsonConverter<AsyncResponseEnvelope<T>>
 91{
 92    private static readonly JsonEncodedText SchemaVersionName = JsonEncodedText.Encode("SchemaVersion");
 93    private static readonly JsonEncodedText SuccessName = JsonEncodedText.Encode("Success");
 94    private static readonly JsonEncodedText PayloadName = JsonEncodedText.Encode("Payload");
 95    private static readonly JsonEncodedText ExceptionMessageName = JsonEncodedText.Encode("ExceptionMessage");
 96    private static readonly JsonEncodedText ExceptionStackTraceName = JsonEncodedText.Encode("ExceptionStackTrace");
 97
 98    /// <summary>Reads the JSON value.</summary>
 99    public override AsyncResponseEnvelope<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions 
 100    {
 101        if (reader.TokenType != JsonTokenType.StartObject)
 102        {
 103            throw new JsonException();
 104        }
 105
 106        int schemaVersion = default;
 107        bool hasSchemaVersion = false;
 108        bool success = false;
 109        T? payload = default;
 110        string? exceptionMessage = null;
 111        string? exceptionStackTrace = null;
 112
 113        while (reader.Read())
 114        {
 115            if (reader.TokenType == JsonTokenType.EndObject)
 116                break;
 117
 118            if (reader.TokenType == JsonTokenType.PropertyName)
 119            {
 120                var property = GetProperty(ref reader);
 121                reader.Read();
 122
 123                if (property == EnvelopeProperty.SchemaVersion)
 124                {
 125                    if (reader.TokenType != JsonTokenType.Number || !reader.TryGetInt32(out schemaVersion))
 126                        throw new JsonException("SchemaVersion must be an integer.");
 127                    hasSchemaVersion = true;
 128                }
 129                else if (property == EnvelopeProperty.Success)
 130                {
 131                    success = reader.GetBoolean();
 132                }
 133                else if (property == EnvelopeProperty.Payload)
 134                {
 135                    // Instead of throwing, assign default(T)
 136                    payload = reader.TokenType == JsonTokenType.Null
 137                        ? default
 138                        : JsonSerializer.Deserialize(ref reader, AsyncResponseJson.GetTypeInfo<T>(options));
 139                }
 140                else if (property == EnvelopeProperty.ExceptionMessage)
 141                {
 142                    exceptionMessage = reader.TokenType == JsonTokenType.Null ? null : reader.GetString();
 143                }
 144                else if (property == EnvelopeProperty.ExceptionStackTrace)
 145                {
 146                    exceptionStackTrace = reader.TokenType == JsonTokenType.Null ? null : reader.GetString();
 147                }
 148                else
 149                {
 150                    reader.Skip();
 151                }
 152            }
 153        }
 154
 155        if (!hasSchemaVersion)
 156            throw new JsonException("SchemaVersion is required.");
 157
 158        return new AsyncResponseEnvelope<T>
 159        {
 160            SchemaVersion = schemaVersion,
 161            Success = success,
 162            Payload = payload!,
 163            ExceptionMessage = exceptionMessage,
 164            ExceptionStackTrace = exceptionStackTrace
 165        };
 166    }
 167
 168    /// <summary>Writes the JSON value.</summary>
 169    public override void Write(Utf8JsonWriter writer, AsyncResponseEnvelope<T> value, JsonSerializerOptions options)
 170    {
 171        writer.WriteStartObject();
 172        writer.WriteNumber(SchemaVersionName, value.SchemaVersion);
 173        writer.WriteBoolean(SuccessName, value.Success);
 174        writer.WritePropertyName(PayloadName);
 175        JsonSerializer.Serialize(writer, (object?)value.Payload, AsyncResponseJson.GetTypeInfo(typeof(T), options));
 176        writer.WriteString(ExceptionMessageName, value.ExceptionMessage);
 177        writer.WriteString(ExceptionStackTraceName, value.ExceptionStackTrace);
 178        writer.WriteEndObject();
 179    }
 180
 181    private static EnvelopeProperty GetProperty(ref Utf8JsonReader reader)
 182    {
 183        if (!reader.HasValueSequence)
 184        {
 185            var name = reader.ValueSpan;
 186            switch (name.Length)
 187            {
 188                case 13 when name[0] == (byte)'S' && reader.ValueTextEquals("SchemaVersion"u8):
 189                    return EnvelopeProperty.SchemaVersion;
 190                case 7 when name[0] == (byte)'S' && reader.ValueTextEquals("Success"u8):
 191                    return EnvelopeProperty.Success;
 192                case 7 when name[0] == (byte)'P' && reader.ValueTextEquals("Payload"u8):
 193                    return EnvelopeProperty.Payload;
 194                case 16 when name[0] == (byte)'E' && reader.ValueTextEquals("ExceptionMessage"u8):
 195                    return EnvelopeProperty.ExceptionMessage;
 196                case 19 when name[0] == (byte)'E' && reader.ValueTextEquals("ExceptionStackTrace"u8):
 197                    return EnvelopeProperty.ExceptionStackTrace;
 198            }
 199        }
 200
 201        if (reader.ValueTextEquals("SchemaVersion"u8))
 202            return EnvelopeProperty.SchemaVersion;
 203        if (reader.ValueTextEquals("Success"u8))
 204            return EnvelopeProperty.Success;
 205        if (reader.ValueTextEquals("Payload"u8))
 206            return EnvelopeProperty.Payload;
 207        if (reader.ValueTextEquals("ExceptionMessage"u8))
 208            return EnvelopeProperty.ExceptionMessage;
 209        if (reader.ValueTextEquals("ExceptionStackTrace"u8))
 210            return EnvelopeProperty.ExceptionStackTrace;
 211
 212        return EnvelopeProperty.Unknown;
 213    }
 214
 215    private enum EnvelopeProperty
 216    {
 217        Unknown,
 218        SchemaVersion,
 219        Success,
 220        Payload,
 221        ExceptionMessage,
 222        ExceptionStackTrace
 223    }
 224}

Methods/Properties

.ctor()