diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs index b7e5ee324d1246..0ce9fa2d743c7c 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs @@ -1015,34 +1015,37 @@ private void WriteMemberElementsIf(Member[] expectedMembers, Member? anyElementM break; case TypeKind.Serializable: SerializableMapping sm = (SerializableMapping)element.Mapping; - // check to see if we need to do the derivation - bool flag = true; + bool isWrappedAny = !element.Any && IsWildcard(sm); + // Check to see if we need to do the derivation, i.e. the actual xsi:type + // refers to a type derived from the declared SerializableMapping type. if (sm.DerivedMappings != null) { XmlQualifiedName? tser = GetXsiType(); - if (tser == null || QNameEqual(tser, sm.XsiType!.Name, defaultNamespace)) + if (tser == null || QNameEqual(tser, sm.XsiType!.Name, sm.XsiType.Namespace)) { + value = ReadSerializable((IXmlSerializable)ReflectionCreateObject(sm.TypeDesc!.Type!)!, isWrappedAny); + } + else if (ReadDerivedSerializable(sm, sm, tser, isWrappedAny, out object? derivedValue)) + { + value = derivedValue; } else { - flag = false; + // The xsi:type matched neither the declared serializable type nor any + // known derived type. Mirror XmlSerializationReaderILGen, which emits + // only Reader.UnknownNode(null) here and leaves the member untouched + // rather than overwriting it with null. Still perform the choice and + // specified bookkeeping the ILGen reader does unconditionally. + UnknownNode(null); + member?.ChoiceSource?.Invoke(elementIndex); + member?.CheckSpecifiedSource?.Invoke(true); + return value; } } - - if (flag) + else { - bool isWrappedAny = !element.Any && IsWildcard(sm); value = ReadSerializable((IXmlSerializable)ReflectionCreateObject(sm.TypeDesc!.Type!)!, isWrappedAny); } - - if (sm.DerivedMappings != null) - { - // https://github.com/dotnet/runtime/issues/1401: - // To Support SpecialMapping Types Having DerivedMappings - throw new NotImplementedException("sm.DerivedMappings != null"); - //WriteDerivedSerializable(sm, sm, source, isWrappedAny); - //WriteUnknownNode("UnknownNode", "null", null, true); - } break; default: throw new InvalidOperationException(SR.XmlInternalError); @@ -1068,6 +1071,39 @@ private void WriteMemberElementsIf(Member[] expectedMembers, Member? anyElementM return value; } + // Walks the SerializableMapping derivation tree looking for the mapping whose XsiType matches + // the supplied xsi:type. Mirrors XmlSerializationReaderILGen.WriteDerivedSerializable. Returns + // true and the deserialized object when a derived mapping matches; otherwise false. + private bool ReadDerivedSerializable(SerializableMapping head, SerializableMapping mapping, XmlQualifiedName? tser, bool isWrappedAny, out object? value) + { + for (SerializableMapping? derived = mapping.DerivedMappings; derived != null; derived = derived.NextDerivedMapping) + { + if (tser == null || QNameEqual(tser, derived.XsiType!.Name, derived.XsiType.Namespace)) + { + if (derived.Type == null) + { + throw CreateMissingIXmlSerializableType(derived.XsiType!.Name, derived.XsiType.Namespace, head.Type!.FullName); + } + + if (!head.Type!.IsAssignableFrom(derived.Type)) + { + throw CreateBadDerivationException(derived.XsiType!.Name, derived.XsiType.Namespace, head.XsiType!.Name, head.XsiType.Namespace, derived.Type.FullName, head.Type.FullName); + } + + value = ReadSerializable((IXmlSerializable)ReflectionCreateObject(derived.TypeDesc!.Type!)!, isWrappedAny); + return true; + } + + if (ReadDerivedSerializable(head, derived, tser, isWrappedAny, out value)) + { + return true; + } + } + + value = null; + return false; + } + private XmlSerializationReadCallback CreateXmlSerializationReadCallback(TypeMapping mapping) { if (mapping is StructMapping structMapping) diff --git a/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj b/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj index b36f101e0cacad..fd0263004573b2 100644 --- a/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj +++ b/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj @@ -439,7 +439,6 @@ - diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs deleted file mode 100644 index 0377bcad9840c4..00000000000000 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; -using SerializationTypes; -using Xunit; - -public static partial class XmlSerializerTests -{ - // Move this test to XmlSerializerTests.cs once #1401 is fixed for the ReflectionOnly serializer. - [Fact] - public static void Xml_DerivedIXmlSerializable() - { - var dClass = new XmlSerializableDerivedClass() { AttributeString = "derivedIXmlSerTest", DateTimeValue = DateTime.Parse("Dec 31, 1999"), BoolValue = true }; - - var expectedXml = WithXmlHeader(@$""); - var fromBase = SerializeAndDeserialize(dClass, expectedXml, () => new XmlSerializer(typeof(XmlSerializableBaseClass), new Type[] { typeof(XmlSerializableDerivedClass) })); - Assert.Equal(dClass.AttributeString, fromBase.AttributeString); - Assert.Equal(dClass.DateTimeValue, fromBase.DateTimeValue); - Assert.Equal(dClass.BoolValue, fromBase.BoolValue); - - // Derived class does not apply XmlRoot attribute to force itself to be emitted with the base class element name, so update expected xml accordingly. - // Since we can't smartly emit xsi:type during serialization though, it is still there even though it isn't needed. - expectedXml = WithXmlHeader(@""); - var fromDerived = SerializeAndDeserialize(dClass, expectedXml, () => new XmlSerializer(typeof(XmlSerializableDerivedClass))); - Assert.Equal(dClass.AttributeString, fromDerived.AttributeString); - Assert.Equal(dClass.DateTimeValue, fromDerived.DateTimeValue); - Assert.Equal(dClass.BoolValue, fromDerived.BoolValue); - } -} diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs index e614b5eff4026f..e3a3841e909f7f 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs @@ -3907,6 +3907,60 @@ public static void Xml_TypeWithArrayLikeChoiceElement() // which element each value was read from. Assert.Equal(new ArrayLikeChoice[] { ArrayLikeChoice.Word, ArrayLikeChoice.Numbers }, actual.ChoiceArray); } + + [Fact] + public static void Xml_DerivedIXmlSerializable() + { + var dClass = new XmlSerializableDerivedClass() { AttributeString = "derivedIXmlSerTest", DateTimeValue = new DateTime(1999, 12, 31), BoolValue = true }; + + var expectedXml = WithXmlHeader(@$""); + var fromBase = SerializeAndDeserialize(dClass, expectedXml, () => new XmlSerializer(typeof(XmlSerializableBaseClass), new Type[] { typeof(XmlSerializableDerivedClass) })); + Assert.Equal(dClass.AttributeString, fromBase.AttributeString); + Assert.Equal(dClass.DateTimeValue, fromBase.DateTimeValue); + Assert.Equal(dClass.BoolValue, fromBase.BoolValue); + + // Derived class does not apply XmlRoot attribute to force itself to be emitted with the base class element name, so update expected xml accordingly. + // Since we can't smartly emit xsi:type during serialization though, it is still there even though it isn't needed. + expectedXml = WithXmlHeader(@""); + var fromDerived = SerializeAndDeserialize(dClass, expectedXml, () => new XmlSerializer(typeof(XmlSerializableDerivedClass))); + Assert.Equal(dClass.AttributeString, fromDerived.AttributeString); + Assert.Equal(dClass.DateTimeValue, fromDerived.DateTimeValue); + Assert.Equal(dClass.BoolValue, fromDerived.BoolValue); + } + + [Fact] + public static void Xml_DerivedIXmlSerializable_UnknownXsiTypeDoesNotClobberMember() + { + var serializer = new XmlSerializer(typeof(XmlSerializableMemberWrapper), new Type[] { typeof(XmlSerializableDerivedClass) }); + + // Produce valid XML carrying a real xsi:type for the IXmlSerializable member, then swap the + // xsi:type to a name that matches neither the declared serializable type nor any known derived + // type. On this path the ILGen reader emits only Reader.UnknownNode(null) and leaves the member + // untouched; the reflection reader must behave identically rather than overwriting it with null. + var wrapper = new XmlSerializableMemberWrapper() + { + Member = new XmlSerializableDerivedClass() { AttributeString = "derived", DateTimeValue = new DateTime(1999, 12, 31), BoolValue = true } + }; + + string validXml; + using (var sw = new StringWriter()) + { + serializer.Serialize(sw, wrapper); + validXml = sw.ToString(); + } + + string unknownTypeXml = validXml.Replace(@"xsi:type=""DerivedIXmlSerializable""", @"xsi:type=""NonExistentDerivedType"""); + Assert.DoesNotContain(@"xsi:type=""DerivedIXmlSerializable""", unknownTypeXml); + + XmlSerializableMemberWrapper result; + using (var sr = new StringReader(unknownTypeXml)) + { + result = (XmlSerializableMemberWrapper)serializer.Deserialize(sr); + } + + Assert.NotNull(result.Member); + Assert.Equal(XmlSerializableMemberWrapper.PresetAttributeString, result.Member.AttributeString); + } } // These types must be declared at the top level rather than nested inside XmlSerializerTests. @@ -3934,3 +3988,17 @@ public enum ArrayLikeChoice Word, Numbers } + +public class XmlSerializableMemberWrapper +{ + public XmlSerializableMemberWrapper() + { + // Pre-populate the member so a regression that overwrites it with null when an + // unrecognized xsi:type is encountered can be detected. + Member = new XmlSerializableBaseClass() { AttributeString = PresetAttributeString }; + } + + public const string PresetAttributeString = "preset"; + + public XmlSerializableBaseClass Member { get; set; } +}