Skip to content

Follow-up: JsonTypeInfo<T> source-gen (AOT) for MTConnect JSON hot-path types #252

Description

@ottobolyos

Origin

Ultrareview dime cycle 1 on #249 (fix/json-serializer-options-per-call-leak). Recorded as TRACKED per CONVENTIONS §1.0d-trigies-septies — belongs in its own campaign PR (touches every serialised type in both JSON assemblies), split out of #249's atomic scope.

Problem

#249 freezes the JsonSerializerOptions singletons so their reflection metadata cache is built once. That eliminates the per-call LCG-DynamicMethod leak observed on DIME production hosts. But the underlying serializer still uses runtime reflection to introspect every reachable type in the MTConnect graph on first serialization of that type — 10 Streams.Json classes in MTConnect.NET-JSON, 25 in MTConnect.NET-JSON-cppagent, plus the Devices/Header/Assets/Errors trees.

That reflection cost:

  • Pays a large first-request latency the first time each response type is serialised.
  • Blocks AOT scenarios (ReadyToRun, NativeAOT, iOS/tvOS mono, .NET 10 AOT publish) — the trimmer + AOT compiler cannot see through the runtime reflection, so trimmed AOT builds silently drop the metadata and fail to serialise at runtime.
  • Emits IL2026 / IL3050 trimming warnings under PublishTrimmed=true, currently muted because the whole assembly isn't AOT-audited.

Proposal

Introduce System.Text.Json source generation across both assemblies:

  • Add a [JsonSerializable(typeof(...))]-marked JsonSerializerContext per assembly: MTConnectJsonContext in MTConnect.NET-JSON, MTConnectCppagentJsonContext in MTConnect.NET-JSON-cppagent.
  • Enumerate every serialised type on the hot path (grep every JsonSerializer.Serialize / Deserialize call, walk from the root Response/Streams/Devices/Assets/Errors types, close the transitive closure).
  • Wire the generated context into JsonFunctions.DefaultOptions / IndentOptions via TypeInfoResolver on both singletons.
  • Set TrimmerRootAssembly / PublishTrimmed=true on a representative agent host and confirm the trimmer + AOT publish walk succeeds with zero IL2026 / IL3050 warnings on the JSON path.
  • RED-first: pin every root Response type has a JsonTypeInfo<T> on the context; pin trimmed publish output produces byte-identical JSON to the reflection path.

Non-goals

  • Not in scope: source-gen for the XML surrogate assemblies (separate campaign, different serializer stack).
  • Not in scope: switching the default resolver at runtime — the source-gen context should be additive (chained onto DefaultJsonTypeInfoResolver), not replacing.

Acceptance

  • Both assemblies expose a JsonSerializerContext covering every serialised type in the reachable graph.
  • Reflection-path baseline stays byte-identical (regression pins).
  • PublishTrimmed=true on a representative host publishes cleanly with zero IL2026 / IL3050 warnings from the JSON assemblies.
  • Full unit + E2E suites green on bluefin.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions