Repository navigation
Description
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 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
- Review duplication findings
- Add
ReadFieldshelper toBaseSerializerorNamedPipeSerializer<T> - Add
WriteListPayload<T>helper toBaseSerializer - 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
Activity