Implement support for derived IXmlSerializable types in XmlSerializer - #129492
#129492Implement support for derived IXmlSerializable types in XmlSerializer#129492StephenMolloy merged 8 commits intodotnet:maindotnet/runtime:mainfrom StephenMolloy:1401-Handle-Derived-Mappings-in-Reflection-XmlSerializerStephenMolloy/runtime:1401-Handle-Derived-Mappings-in-Reflection-XmlSerializerCopy head branch name to clipboard
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request updates the reflection-based XmlSerializer reader to support deserializing derived IXmlSerializable types by resolving xsi:type against SerializableMapping.DerivedMappings, and enables the previously-skipped derived-IXmlSerializable test in the main test suite.
Changes:
- Update
ReflectionXmlSerializationReaderto choose the correct derivedSerializableMappingbased onxsi:type(including walking the derived-mapping tree). - Add a helper (
ReadDerivedSerializable) to perform the derived-mapping lookup + instantiation/deserialization. - Move/enable the
Xml_DerivedIXmlSerializabletest inXmlSerializerTests.cs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs | Adds derived IXmlSerializable dispatch by walking SerializableMapping.DerivedMappings based on xsi:type. |
| src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs | Enables a regression test validating derived IXmlSerializable serialization/deserialization scenarios. |
| src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs | Removes the derived IXmlSerializable test from the internal/skip-only suite now that the feature is implemented. |
…rializable Mirror XmlSerializationReaderILGen in the reflection-based reader: when an unknown xsi:type is encountered for a serializable member with derived mappings, consume the node and perform choice/specified bookkeeping but skip the member value assignment so a pre-populated member is not overwritten with null. Adds a regression test that runs in both the ILGen and ReflectionOnly readers, and replaces a culture-dependent DateTime.Parse in the existing derived-IXmlSerializable test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…terference The XmlSerializableMemberWrapper type lived in the shared SerializationTypes.cs, which is compiled into SerializableAssembly. The pregen-serializer test (SgenCommandTest) runs SGen over that assembly and compares output length against a checked-in golden file, so the added type changed the generated length. Move the derived IXmlSerializable tests and the wrapper type into XmlSerializerTests.RuntimeOnly.cs, which the pregen test project does not compile, so the tests still run in both the ILGen and ReflectionOnly readers without affecting SerializableAssembly or the golden file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-add test that got lost during merge conflict resolution
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "25d4774d0ceb95ea08e34f7ed3330c5a44672c27",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a991d279bd4e8f0aadf0a685f60d6bcafaebaca3",
"last_reviewed_commit": "25d4774d0ceb95ea08e34f7ed3330c5a44672c27",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a991d279bd4e8f0aadf0a685f60d6bcafaebaca3",
"last_recorded_worker_run_id": "29684004083",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "25d4774d0ceb95ea08e34f7ed3330c5a44672c27",
"review_id": 4730639078
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: Justified. The reflection-based XmlSerializer reader previously threw NotImplementedException (or silently clobbered the member with null) when an xsi:type referred to a derived IXmlSerializable type, closing the long-standing gap tracked by #1401. This is real, observable behavior that diverged from the RefEmit (ILGen) reader.
Approach: Sound. The new ReadDerivedSerializable walks the SerializableMapping.DerivedMappings tree and mirrors XmlSerializationReaderILGen.WriteDerivedSerializable closely — same traversal order (each derived mapping, then its children, then the next sibling), the same CreateMissingIXmlSerializableType/CreateBadDerivationException error semantics, and the same "emit UnknownNode(null) and leave the member untouched" behavior on an unrecognized xsi:type. It also corrects a pre-existing bug where the base-type match used defaultNamespace instead of sm.XsiType.Namespace, aligning with the ILGen WriteQNameEqual("tser", sm.XsiType.Name, sm.XsiType.Namespace).
Summary: ✅ LGTM. The reflection reader now matches the RefEmit reference implementation for derived IXmlSerializable types, the namespace-comparison fix is correct, and the choice/specified bookkeeping on the unknown-type path faithfully reproduces the unconditional checkSpecified/choice sets that WriteElement performs in ILGen. Tests exercise both the success and no-clobber regression paths, and because they live in XmlSerializerTests.RuntimeOnly.cs they run under both the RefEmit and Reflection (Mode=1) serializers, giving genuine coverage of the new code.
Detailed Findings
✅ Correctness — Faithful mirror of the ILGen reader
The recursion in ReadDerivedSerializable reproduces WriteDerivedSerializable: it matches on tser == null || QNameEqual(tser, derived.XsiType.Name, derived.XsiType.Namespace), throws CreateMissingIXmlSerializableType when derived.Type == null, throws CreateBadDerivationException when the derived type is not assignable to the head type, and otherwise reads it. Traversal order (self, then children, then next sibling) matches the ILGen else-if chain generation, so overload resolution across a multi-level derivation tree is preserved.
✅ Correctness — Unknown-type path does not clobber the member
On the no-match branch the code calls UnknownNode(null) and invokes member?.ChoiceSource / member?.CheckSpecifiedSource before returning without calling member.Source, so the existing member value is preserved. This reproduces ILGen, where WriteElement sets checkSpecified unconditionally at the top and the choice source at the bottom, while the unmatched xsi:type only emits Reader.UnknownNode(null). The Xml_DerivedIXmlSerializable_UnknownXsiTypeDoesNotClobberMember test verifies the preserved value.
✅ Test quality — Coverage in both serializer modes
Moving Xml_DerivedIXmlSerializable out of XmlSerializerTests.Internal.cs (RefEmit-only) into XmlSerializerTests.RuntimeOnly.cs means it now also compiles into the ReflectionOnly test project, which sets XmlSerializer Mode=1, so the derived-type path is validated against the new reflection code as well as the RefEmit baseline. The added no-clobber test uses a wrapper whose constructor pre-populates the member, then string-swaps a valid xsi:type to an unknown one — a clean, targeted regression for the previous null-clobbering behavior.
💡 Observation — Value-type IXmlSerializable
Both the base and derived reads go through (IXmlSerializable)ReflectionCreateObject(...), which boxes value-type IXmlSerializable implementers. The ILGen reader special-cases IsValueType with an explicit ConvertValue, but the reflection reader’s object-based flow already handles this via the cast and member.Source assignment, so no change is needed; noting only for completeness.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 191.3 AIC · ⌖ 10.9 AIC · ⊞ 10K
|
@mangod9 , this one needed to resolve a merge conflict in our test project. (The result is an empty class, I know. It's test code we can clean up later. I didn't want to figure out how to delete an entire file and modify the test project file in the web merge editor.) Resolving the conflict requires another approval. :/ |
Actually... hang on. There might be a hidden merge conflict going on here as well with choice identifiers. Update: resolved |
mconnew
left a comment
There was a problem hiding this comment.
Only comment I had was really one of style, not correctness.
This pull request improves support for deserializing derived types that implement
IXmlSerializablein the reflection-based XML serializer. It fixes a long-standing issue by enabling proper handling ofxsi:typefor derived serializable types, and moves the corresponding test to the main test suite. The most important changes are:ReflectionXmlSerializationReader improvements:
SerializableMappingderivation tree and correctly deserialize objects when anxsi:typerefers to a derivedIXmlSerializabletype, instead of throwing aNotImplementedExceptionor failing to handle the type. [1] [2]Test suite updates:
Xml_DerivedIXmlSerializabletest fromXmlSerializerTests.Internal.cs(where it was skipped due to the missing feature) to the mainXmlSerializerTests.csfile, verifying that derivedIXmlSerializabletypes are now correctly serialized and deserialized. [1] [2]Fixes: #1401