Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
mangod9 marked this conversation as resolved.
Comment thread
StephenMolloy marked this conversation as resolved.
return value;
}
Comment thread
StephenMolloy marked this conversation as resolved.
}

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);
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@

<ItemGroup>
<Compile Include="XmlSerializer\XmlSerializerTests.cs" />
<Compile Include="XmlSerializer\XmlSerializerTests.Internal.cs" />
<Compile Include="XmlSerializer\XmlSerializerTests.RuntimeOnly.cs" />
<Compile Include="..\..\System.Runtime.Serialization.Xml\tests\SerializationTypes.RuntimeOnly.cs" />
<None Include="..\..\System.Runtime.Serialization.Xml\tests\SerializationTypes.cs" />
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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(@$"<BaseIXmlSerializable xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""DerivedIXmlSerializable"" AttributeString=""derivedIXmlSerTest"" DateTimeValue=""1999-12-31T00:00:00"" BoolValue=""True"" xmlns=""{XmlSerializableBaseClass.XmlNamespace}"" />");
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(@"<DerivedIXmlSerializable xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""DerivedIXmlSerializable"" AttributeString=""derivedIXmlSerTest"" DateTimeValue=""1999-12-31T00:00:00"" BoolValue=""True"" />");
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.
Expand Down Expand Up @@ -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; }
}
Loading