From 52ff0f6f0a286f2753ea9ecb9af8abc51444eb60 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Wed, 13 May 2026 07:18:45 -0500 Subject: [PATCH] Annotate Serialization/Serializer.cs reflective surface for AOT (closes #70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First Polecat AOT-pillar slice (polecat#70), mirroring the methodology that closed JasperFx slices #252-#255 + the tail in JasperFx#260. ISerializer interface annotated ToJson(object) — [RUC] + [RDC] FromJson(string|Stream|DbDataReader) — [RUC] + [RDC] FromJson(Type, string|Stream|DbDataReader) — [RUC] + [RDC] FromJsonAsync(Stream, CancellationToken) — [RUC] + [RDC] FromJsonAsync(Type, Stream, CancellationToken) — [RUC] + [RDC] Interface XML docs updated with the AOT story: AOT-publishing apps should supply a custom ISerializer implementation backed by a System.Text.Json source-generator JsonSerializerContext rather than the reflection-based default. Serializer impl Matching [RequiresUnreferencedCode] + [RequiresDynamicCode] on every override (required to keep IL2046 quiet). Suppressed with justification ApplyEnumStorage — IL3050 on JsonStringEnumConverter (non-generic enum converter; AOT consumers use the generic JsonStringEnumConverter) ApplyNonPublicMembers — IL2026 + IL3050 + IL2075 on DefaultJsonTypeInfoResolver + fallback GetProperties scan. The NonPublicMembers feature is reflection-driven by intent; opting into it is opt-in to reflection. Effect on the punch list (per TFM) Serializer.cs slice: 36 → 0 (closed) Polecat total: 270 → 422 (+152 cascade) The +152 cascade is honest propagation, not regression. With the un-annotated Serializer, every caller hid behind it; now the trim-impact surface is explicit at the call sites. The cascade lands in files already targeted by other AOT-pillar issues: DocumentStore.ProjectionReplay.cs — covered by polecat#71 Linq/PolecatQueryableExtensions.cs / LinqQueryProvider.cs — covered by polecat#72 Storage/DocumentMapping.cs + DocumentProviderRegistry — covered by polecat#73 Plus some new cascade in transitive callers (Patching/PatchExpression.cs, Events/EventOperations.cs, Internal/QuerySession.cs, Internal/DocumentProvider.cs) that will be absorbed into a future Polecat tail-cleanup PR mirroring JasperFx#260. Verification Polecat.csproj + Polecat.Tests.csproj build clean (0 errors) Closes #70. --- src/Polecat/Serialization/ISerializer.cs | 27 ++++++++++++++++++++++++ src/Polecat/Serialization/Serializer.cs | 27 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Polecat/Serialization/ISerializer.cs b/src/Polecat/Serialization/ISerializer.cs index c506dea..f308c7f 100644 --- a/src/Polecat/Serialization/ISerializer.cs +++ b/src/Polecat/Serialization/ISerializer.cs @@ -1,10 +1,19 @@ using System.Data.Common; +using System.Diagnostics.CodeAnalysis; namespace Polecat.Serialization; /// /// Serialization abstraction for Polecat. Uses System.Text.Json exclusively. /// +/// +/// The default implementation routes through +/// System.Text.Json.JsonSerializer, which the .NET trimmer cannot +/// statically analyse. AOT-publishing apps should plug in a custom +/// ISerializer implementation backed by an STJ source-generator +/// context (JsonSerializerContext) rather than the reflection-based +/// default. +/// public interface ISerializer { /// @@ -20,45 +29,63 @@ public interface ISerializer /// /// Serialize an object to a JSON string. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection over document.GetType()'s properties; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] string ToJson(object document); /// /// Deserialize a JSON string to an object of type T. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] T FromJson(string json); /// /// Deserialize a JSON string to an object of the specified type. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] object FromJson(Type type, string json); /// /// Deserialize from a Stream. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] T FromJson(Stream stream); /// /// Deserialize from a Stream to the specified type. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] object FromJson(Type type, Stream stream); /// /// Deserialize from a DbDataReader column. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] T FromJson(DbDataReader reader, int index); /// /// Deserialize from a DbDataReader column to the specified type. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] object FromJson(Type type, DbDataReader reader, int index); /// /// Async deserialize from a Stream. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] ValueTask FromJsonAsync(Stream stream, CancellationToken cancellationToken = default); /// /// Async deserialize from a Stream to the specified type. /// + [RequiresUnreferencedCode("Default ISerializer uses STJ reflection; AOT consumers should supply a source-generator-backed ISerializer impl.")] + [RequiresDynamicCode("Default ISerializer uses STJ reflection which requires runtime code generation.")] ValueTask FromJsonAsync(Type type, Stream stream, CancellationToken cancellationToken = default); } diff --git a/src/Polecat/Serialization/Serializer.cs b/src/Polecat/Serialization/Serializer.cs index ab6aab4..871a72e 100644 --- a/src/Polecat/Serialization/Serializer.cs +++ b/src/Polecat/Serialization/Serializer.cs @@ -1,4 +1,5 @@ using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -74,48 +75,66 @@ public void Configure(Action configure) configure(_options); } + [RequiresUnreferencedCode("STJ JsonSerializer.Serialize uses reflection over document's runtime type.")] + [RequiresDynamicCode("STJ JsonSerializer.Serialize requires runtime code generation for non-source-generated types.")] public string ToJson(object document) { return JsonSerializer.Serialize(document, document.GetType(), _options); } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over T.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public T FromJson(string json) { return JsonSerializer.Deserialize(json, _options)!; } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over the supplied type.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public object FromJson(Type type, string json) { return JsonSerializer.Deserialize(json, type, _options)!; } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over T.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public T FromJson(Stream stream) { return JsonSerializer.Deserialize(stream, _options)!; } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over the supplied type.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public object FromJson(Type type, Stream stream) { return JsonSerializer.Deserialize(stream, type, _options)!; } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over T.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public T FromJson(DbDataReader reader, int index) { var json = reader.GetString(index); return FromJson(json); } + [RequiresUnreferencedCode("STJ JsonSerializer.Deserialize uses reflection over the supplied type.")] + [RequiresDynamicCode("STJ JsonSerializer.Deserialize requires runtime code generation for non-source-generated types.")] public object FromJson(Type type, DbDataReader reader, int index) { var json = reader.GetString(index); return FromJson(type, json); } + [RequiresUnreferencedCode("STJ JsonSerializer.DeserializeAsync uses reflection over T.")] + [RequiresDynamicCode("STJ JsonSerializer.DeserializeAsync requires runtime code generation for non-source-generated types.")] public async ValueTask FromJsonAsync(Stream stream, CancellationToken cancellationToken = default) { return (await JsonSerializer.DeserializeAsync(stream, _options, cancellationToken))!; } + [RequiresUnreferencedCode("STJ JsonSerializer.DeserializeAsync uses reflection over the supplied type.")] + [RequiresDynamicCode("STJ JsonSerializer.DeserializeAsync requires runtime code generation for non-source-generated types.")] public async ValueTask FromJsonAsync(Type type, Stream stream, CancellationToken cancellationToken = default) { @@ -132,6 +151,8 @@ public static JsonSerializerOptions DefaultOptions() }; } + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "JsonStringEnumConverter(JsonNamingPolicy, bool) requires runtime code generation for non-source-generated enum types. The default Serializer is reflection-based by design; AOT consumers should supply a custom ISerializer with the generic JsonStringEnumConverter per-enum.")] private void ApplyEnumStorage() { // Remove any existing JsonStringEnumConverter @@ -160,6 +181,12 @@ private void ApplyCasing() }; } + [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "DefaultJsonTypeInfoResolver uses reflection. The whole NonPublicMembers feature is reflection-driven by intent; AOT consumers should supply a custom ISerializer with an STJ source-generator context.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", + Justification = "Same as IL2026.")] + [UnconditionalSuppressMessage("Trimming", "IL2075:DynamicallyAccessedMembers", + Justification = "Falls back to scanning typeInfo.Type.GetProperties when the JSON property has no usable AttributeProvider. Reflection on user types — keeping public/non-public properties alive is the user's responsibility when opting into NonPublicMembers.")] private void ApplyNonPublicMembers() { if (_nonPublicMembersStorage == NonPublicMembersStorage.Default)