Skip to content

[duplicate-code] Duplicate Code: DotnetTest IPC Serializer 3-Phase Boilerplate Repeated Across 8 Serializers #9764

Description

@github-actions

Summary

The 8 serializers under src/Platform/Microsoft.Testing.Platform/ServerMode/DotnetTest/IPC/Serializers/ all implement the same 3-phase read/write protocol. Each serializer independently reimplements the same field-loop deserialization, deferred-size-backfill list serialization, and GetFieldCount helper using copy-pasted structural boilerplate — only the field constants and record types differ.

Duplication Details

Pattern: 3-Phase Deserialize / Serialize / GetFieldCount boilerplate

  • Severity: High
  • Occurrences: 8 serializer files (~150–470 lines each)
  • Locations:
File DeserializeCore (phase 1) SerializeCore list loop (phase 2) GetFieldCount helpers (phase 3)
DiscoveredTestMessagesSerializer.cs ~88 ~248–287 ~343–359
TestInProgressMessagesSerializer.cs ~45 ~118–150 ~153–158
TestResultMessagesSerializer.cs ~117 ~335–438 ~441–468
FileArtifactMessagesSerializer.cs ~61 ~150–186 ~189–194
CommandLineOptionMessagesSerializer.cs ~48 ~126–159 ~162–166
AzureDevOpsLogMessageSerializer.cs ~34 implicit ~60
DisplayMessageSerializer.cs ~39 implicit ~65
TestSessionEventSerializer.cs ~34 implicit ~56

Phase 1 — field-loop deserialization (identical in every file, only case constants differ):

ushort fieldCount = ReadUShort(stream);
for (int i = 0; i < fieldCount; i++)
{
    int fieldId = ReadUShort(stream);
    int fieldSize = ReadInt(stream);
    switch (fieldId)
    {
        case SomeFields.FieldXId:
            fieldX = ReadStringValue(stream, fieldSize);
            break;
        // ... more fields ...
        default:
            // If we don't recognize the field id, skip the payload
            SetPosition(stream, stream.Position + fieldSize);
            break;
    }
}

Phase 2 — deferred-size backfill for list payloads (identical in every file):

WriteUShort(stream, listFieldId);
WriteInt(stream, 0);            // size placeholder — filled in below
long before = stream.Position;
WriteInt(stream, list.Length);
foreach (var item in list)
{
    WriteUShort(stream, GetFieldCount(item));
    // ... per-field writes ...
}
// NOTE: We are able to seek only if we are using a MemoryStream
WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int));

Phase 3 — GetFieldCount helper (sum-of-non-null pattern in every file):

private static ushort GetFieldCount(TMessage msg) =>
    (ushort)((msg.FieldA is null ? 0 : 1) +
             (msg.FieldB is null ? 0 : 1) +
             (msg.FieldC is null ? 0 : 1));

Impact Analysis

  • Maintainability: Any change to the wire protocol (e.g., adding forward-compatibility handling, changing the skip-unknown logic) must be replicated manually across all 8 serializers.
  • Bug Risk: A bug in the "skip unknown field" fallback (SetPosition(stream, stream.Position + fieldSize)) or in the backfill (WriteAtPosition) would need to be hunted and fixed in every serializer independently.
  • Code Bloat: Estimated ~400–600 lines of repeated structural code across the 8 files.

Refactoring Recommendations

  1. Generic NamedPipeSerializer<T> with field-descriptor table: Extend the existing NamedPipeSerializer<T> base class (already in Serializers/NamedPipeSerializer.cs) with a ReadFields(Stream, Func<ushort, int, T, T> applier) helper that encapsulates the field-loop including the skip-unknown fallback. Each concrete serializer provides only a thin descriptor table.

  2. WriteListPayload<T> static helper: Extract the deferred-size-backfill pattern into a single protected static WriteListPayload<T>(Stream, IReadOnlyList<T>, Action<T, Stream>) method on BaseSerializer, eliminating the copy-pasted before/WriteAtPosition sequence.

  3. Source-generated GetFieldCount: A simple source generator or T4 template that counts non-null properties on a record type can replace the hand-written GetFieldCount helper in each file.

Implementation Checklist

  • Review duplication findings
  • Add ReadFields helper to BaseSerializer or NamedPipeSerializer<T>
  • Add WriteListPayload<T> helper to BaseSerializer
  • Refactor each of the 8 serializers to use the new helpers
  • Run IPC/dotnet-test integration tests to confirm wire compatibility
  • Verify no serializer behavior changes

Analysis Metadata

  • Analyzed Files: 8 serializer files
  • Detection Method: Semantic code analysis
  • Analysis Date: 2026-07-09

🤖 Automated content by GitHub Copilot. Generated by the Duplicate Code Detector workflow. · 273.3 AIC · ⌖ 9.1 AIC · ⊞ 8K · [◷]( · )

Add this agentic workflows to your repo

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/duplicate-code-detector.md@main
  • expires on Jul 11, 2026, 5:42 AM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions