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

Information
Class: AsyncResponse.AsyncResponseJson.ChainResolver
Assembly: AsyncResponse.Core
File(s): /home/runner/work/AsyncResponse/AsyncResponse/src/AsyncResponse.Core/AsyncResponseJson.cs
Line coverage
100%
Covered lines: 8
Uncovered lines: 0
Coverable lines: 8
Total lines: 129
Line coverage: 100%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GetTypeInfo(...)87.5%88100%

File(s)

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

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json;
 3using System.Text.Json.Serialization;
 4using System.Text.Json.Serialization.Metadata;
 5
 6namespace AsyncResponse;
 7
 8/// <summary>
 9/// The library's single JSON entry point: every internal serialization site goes through these
 10/// options and helpers instead of the reflection-based <see cref="JsonSerializer"/> overloads, so
 11/// the packages carry no trim/AOT warnings (IL2026/IL3050).
 12/// <para>
 13/// Metadata resolution order: library wire types (<see cref="AsyncResponseJsonContext"/>, source
 14/// generated) → user-registered resolvers (<see cref="AsyncResponseJsonSerialization"/>) → the
 15/// runtime reflection resolver when the app has it enabled
 16/// (<see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, true for every non-trimmed app).
 17/// Behavior for existing apps is therefore unchanged; trimmed/AOT apps must register their payload
 18/// types and otherwise get an actionable error naming the type.
 19/// </para>
 20/// </summary>
 21internal static class AsyncResponseJson
 22{
 23    private static readonly IJsonTypeInfoResolver? _reflectionResolver = CreateReflectionResolverIfEnabled();
 24
 25    /// <summary>The full resolver chain, for options that need to prepend their own metadata.</summary>
 26    public static IJsonTypeInfoResolver Resolver { get; } = new ChainResolver();
 27
 28    /// <summary>Serializer-default settings (case-sensitive, write nulls) over the resolver chain.</summary>
 29    public static JsonSerializerOptions Default { get; } = new() { TypeInfoResolver = Resolver };
 30
 31    /// <summary>
 32    /// Case-insensitive property matching, for broker-ingress reads — the historical behavior of
 33    /// the library's defensive deserialization paths.
 34    /// </summary>
 35    public static JsonSerializerOptions CaseInsensitive { get; } = new()
 36    {
 37        TypeInfoResolver = Resolver,
 38        PropertyNameCaseInsensitive = true
 39    };
 40
 41    /// <summary>Omits null properties on write; used for the durable-flow ledger.</summary>
 42    public static JsonSerializerOptions IgnoreNullWrites { get; } = new()
 43    {
 44        TypeInfoResolver = Resolver,
 45        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
 46    };
 47
 48    /// <summary>Serializes with default settings, resolving metadata through the chain.</summary>
 49    public static string Serialize<T>(T value)
 50        => JsonSerializer.Serialize(value, GetTypeInfo<T>(Default));
 51
 52    /// <summary>
 53    /// Serializes by the value's runtime type — the counterpart of the reflection-based
 54    /// <c>JsonSerializer.Serialize(value, value.GetType())</c> pattern.
 55    /// </summary>
 56    public static string Serialize(object value, Type runtimeType)
 57        => JsonSerializer.Serialize(value, GetTypeInfo(runtimeType, Default));
 58
 59    /// <summary>
 60    /// Deserializes with default settings (case-sensitive property matching, like the bare
 61    /// <c>JsonSerializer.Deserialize&lt;T&gt;(json)</c> these callsites used before).
 62    /// </summary>
 63    public static T? Deserialize<T>(string json)
 64        => JsonSerializer.Deserialize(json, GetTypeInfo<T>(Default));
 65
 66    /// <summary>Resolves typed metadata for <typeparamref name="T"/> from <paramref name="options"/>.</summary>
 67    public static JsonTypeInfo<T> GetTypeInfo<T>(JsonSerializerOptions options)
 68        => (JsonTypeInfo<T>)GetTypeInfo(typeof(T), options);
 69
 70    /// <summary>
 71    /// Resolves metadata for <paramref name="type"/> from <paramref name="options"/>, translating
 72    /// the serializer's "no metadata" failure into guidance to register a context.
 73    /// </summary>
 74    public static JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
 75    {
 76        try
 77        {
 78            return options.GetTypeInfo(type);
 79        }
 80        catch (NotSupportedException ex)
 81        {
 82            throw new NotSupportedException(
 83                $"No JSON metadata is available for '{type}'. This app runs without reflection-based " +
 84                "System.Text.Json (trimmed/Native AOT), so payload types must be registered at startup: " +
 85                $"declare [JsonSerializable(typeof({type.Name}))] on a JsonSerializerContext and call " +
 86                $"{nameof(AsyncResponseJsonSerialization)}.{nameof(AsyncResponseJsonSerialization.RegisterResolver)}(You
 87                ex);
 88        }
 89    }
 90
 91    private static IJsonTypeInfoResolver? CreateReflectionResolverIfEnabled()
 92    {
 93        if (!JsonSerializer.IsReflectionEnabledByDefault)
 94            return null;
 95
 96        return CreateReflectionResolver();
 97
 98        [UnconditionalSuppressMessage("Trimming", "IL2026",
 99            Justification = "Reachable only when JsonSerializer.IsReflectionEnabledByDefault is true; trimmed and AOT bu
 100        [UnconditionalSuppressMessage("AOT", "IL3050",
 101            Justification = "Same guard: the feature switch is false under Native AOT, so the reflection resolver is nev
 102        static IJsonTypeInfoResolver CreateReflectionResolver() => new DefaultJsonTypeInfoResolver();
 103    }
 104
 105    /// <summary>
 106    /// Library wire types first (their contract is fixed and must not be overridden), then
 107    /// user-registered resolvers, then the reflection fallback when available. Consulting the
 108    /// live registration snapshot per lookup lets startup-time registration order be forgiving;
 109    /// results are cached per options instance by the serializer itself.
 110    /// </summary>
 111    private sealed class ChainResolver : IJsonTypeInfoResolver
 112    {
 113        public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
 114        {
 3115            var info = ((IJsonTypeInfoResolver)AsyncResponseJsonContext.Default).GetTypeInfo(type, options);
 3116            if (info is not null)
 3117                return info;
 118
 3119            foreach (var resolver in AsyncResponseJsonSerialization.Resolvers)
 120            {
 3121                info = resolver.GetTypeInfo(type, options);
 3122                if (info is not null)
 1123                    return info;
 124            }
 125
 3126            return _reflectionResolver?.GetTypeInfo(type, options);
 127        }
 128    }
 129}