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
-
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.
-
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.
-
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
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
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, andGetFieldCounthelper using copy-pasted structural boilerplate — only the field constants and record types differ.Duplication Details
Pattern: 3-Phase Deserialize / Serialize / GetFieldCount boilerplate
DiscoveredTestMessagesSerializer.csTestInProgressMessagesSerializer.csTestResultMessagesSerializer.csFileArtifactMessagesSerializer.csCommandLineOptionMessagesSerializer.csAzureDevOpsLogMessageSerializer.csDisplayMessageSerializer.csTestSessionEventSerializer.csPhase 1 — field-loop deserialization (identical in every file, only
caseconstants differ):Phase 2 — deferred-size backfill for list payloads (identical in every file):
Phase 3 —
GetFieldCounthelper (sum-of-non-null pattern in every file):Impact Analysis
SetPosition(stream, stream.Position + fieldSize)) or in the backfill (WriteAtPosition) would need to be hunted and fixed in every serializer independently.Refactoring Recommendations
Generic
NamedPipeSerializer<T>with field-descriptor table: Extend the existingNamedPipeSerializer<T>base class (already inSerializers/NamedPipeSerializer.cs) with aReadFields(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.WriteListPayload<T>static helper: Extract the deferred-size-backfill pattern into a single protected staticWriteListPayload<T>(Stream, IReadOnlyList<T>, Action<T, Stream>)method onBaseSerializer, eliminating the copy-pastedbefore/WriteAtPositionsequence.Source-generated
GetFieldCount: A simple source generator or T4 template that counts non-null properties on a record type can replace the hand-writtenGetFieldCounthelper in each file.Implementation Checklist
ReadFieldshelper toBaseSerializerorNamedPipeSerializer<T>WriteListPayload<T>helper toBaseSerializerAnalysis Metadata
Add this agentic workflows to your repo
To install this agentic workflow, run