| | | 1 | | using System.Diagnostics.CodeAnalysis; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using System.Text.Json.Serialization; |
| | | 4 | | using System.Text.Json.Serialization.Metadata; |
| | | 5 | | |
| | | 6 | | namespace 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> |
| | | 21 | | internal static class AsyncResponseJson |
| | | 22 | | { |
| | 3 | 23 | | private static readonly IJsonTypeInfoResolver? _reflectionResolver = CreateReflectionResolverIfEnabled(); |
| | | 24 | | |
| | | 25 | | /// <summary>The full resolver chain, for options that need to prepend their own metadata.</summary> |
| | 3 | 26 | | public static IJsonTypeInfoResolver Resolver { get; } = new ChainResolver(); |
| | | 27 | | |
| | | 28 | | /// <summary>Serializer-default settings (case-sensitive, write nulls) over the resolver chain.</summary> |
| | 3 | 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> |
| | 3 | 35 | | public static JsonSerializerOptions CaseInsensitive { get; } = new() |
| | 3 | 36 | | { |
| | 3 | 37 | | TypeInfoResolver = Resolver, |
| | 3 | 38 | | PropertyNameCaseInsensitive = true |
| | 3 | 39 | | }; |
| | | 40 | | |
| | | 41 | | /// <summary>Omits null properties on write; used for the durable-flow ledger.</summary> |
| | 3 | 42 | | public static JsonSerializerOptions IgnoreNullWrites { get; } = new() |
| | 3 | 43 | | { |
| | 3 | 44 | | TypeInfoResolver = Resolver, |
| | 3 | 45 | | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
| | 3 | 46 | | }; |
| | | 47 | | |
| | | 48 | | /// <summary>Serializes with default settings, resolving metadata through the chain.</summary> |
| | | 49 | | public static string Serialize<T>(T value) |
| | 3 | 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) |
| | 3 | 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<T>(json)</c> these callsites used before). |
| | | 62 | | /// </summary> |
| | | 63 | | public static T? Deserialize<T>(string json) |
| | 3 | 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) |
| | 3 | 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 | | { |
| | 3 | 78 | | return options.GetTypeInfo(type); |
| | | 79 | | } |
| | 1 | 80 | | catch (NotSupportedException ex) |
| | | 81 | | { |
| | 1 | 82 | | throw new NotSupportedException( |
| | 1 | 83 | | $"No JSON metadata is available for '{type}'. This app runs without reflection-based " + |
| | 1 | 84 | | "System.Text.Json (trimmed/Native AOT), so payload types must be registered at startup: " + |
| | 1 | 85 | | $"declare [JsonSerializable(typeof({type.Name}))] on a JsonSerializerContext and call " + |
| | 1 | 86 | | $"{nameof(AsyncResponseJsonSerialization)}.{nameof(AsyncResponseJsonSerialization.RegisterResolver)}(You |
| | 1 | 87 | | ex); |
| | | 88 | | } |
| | 3 | 89 | | } |
| | | 90 | | |
| | | 91 | | private static IJsonTypeInfoResolver? CreateReflectionResolverIfEnabled() |
| | | 92 | | { |
| | 3 | 93 | | if (!JsonSerializer.IsReflectionEnabledByDefault) |
| | 1 | 94 | | return null; |
| | | 95 | | |
| | 2 | 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 |
| | 2 | 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 | | { |
| | 3 | 115 | | var info = ((IJsonTypeInfoResolver)AsyncResponseJsonContext.Default).GetTypeInfo(type, options); |
| | 3 | 116 | | if (info is not null) |
| | 3 | 117 | | return info; |
| | | 118 | | |
| | 3 | 119 | | foreach (var resolver in AsyncResponseJsonSerialization.Resolvers) |
| | | 120 | | { |
| | 3 | 121 | | info = resolver.GetTypeInfo(type, options); |
| | 3 | 122 | | if (info is not null) |
| | 1 | 123 | | return info; |
| | | 124 | | } |
| | | 125 | | |
| | 3 | 126 | | return _reflectionResolver?.GetTypeInfo(type, options); |
| | | 127 | | } |
| | | 128 | | } |
| | | 129 | | } |