Skip to content

Commit cc2019b

Browse files
Reflection-based XmlSerializer - Deserialize empty collections and allow for sub-types in collection items. (#111723)
* Add tests to verify issue and fix. * Check for type-assignability instead of equivallence. Also fix Choice logic. * Add tests to verify issue and fix. * Ensure collections are initialized to empty - even if they should be null according to the xml. * More tests for bringing reflection-based serializer to quirky parity with ILGen. * Fix Xml/Type mapping to use proper member info with hidden/inherited type members; Fix List-handling in reflection serializer. * Cleanup tests and comments based on PR feedback. * While considering initializing empty lists at object creation instead of post-deserialization - noticed a missed empty-list case.
1 parent 8b6071e commit cc2019b

File tree

11 files changed

+4464
-2461
lines changed

11 files changed

+4464
-2461
lines changed

src/libraries/Microsoft.XmlSerializer.Generator/tests/Expected.SerializableAssembly.XmlSerializers.cs

Lines changed: 3318 additions & 2400 deletions
Large diffs are not rendered by default.

src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,31 @@ internal MemberMapping Clone()
981981
{
982982
return new MemberMapping(this);
983983
}
984+
985+
internal bool Hides(MemberMapping mapping)
986+
{
987+
// Semantics. But all mappings would hide a null mapping, so indicating that this particular instance of mapping
988+
// specifically hides a null mapping is not necessary and could be misleading. So just return false.
989+
if (mapping == null)
990+
return false;
991+
992+
var baseMember = mapping.MemberInfo;
993+
var derivedMember = this.MemberInfo;
994+
995+
// Similarly, if either mapping (or both) lacks MemberInfo, then its not appropriate to say that one hides the other.
996+
if (baseMember == null || derivedMember == null)
997+
return false;
998+
999+
// If the member names are different, they don't hide each other
1000+
if (baseMember.Name != derivedMember.Name)
1001+
return false;
1002+
1003+
// Names are the same. So, regardless of field or property or accessibility or return type, if derivedDeclaringType is a
1004+
// subclass of baseDeclaringType, then the base member is hidden.
1005+
Type? baseDeclaringType = baseMember.DeclaringType;
1006+
Type? derivedDeclaringType = derivedMember.DeclaringType;
1007+
return baseDeclaringType != null && derivedDeclaringType != null && baseDeclaringType.IsAssignableFrom(derivedDeclaringType);
1008+
}
9841009
}
9851010

9861011
internal sealed class MembersMapping : TypeMapping

src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1633,6 +1633,11 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje
16331633
{
16341634
if (member.Source == null && mapping.TypeDesc.IsArrayLike && !(mapping.Elements!.Length == 1 && mapping.Elements[0].Mapping is ArrayMapping))
16351635
{
1636+
// Always create a collection for (non-array) collection-like types, even if the XML data says the collection should be null.
1637+
if (!mapping.TypeDesc.IsArray)
1638+
{
1639+
member.Collection ??= new CollectionMember();
1640+
}
16361641
member.Source = (item) =>
16371642
{
16381643
member.Collection ??= new CollectionMember();
@@ -1645,26 +1650,51 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje
16451650

16461651
if (member.Source == null)
16471652
{
1653+
var isList = mapping.TypeDesc.IsArrayLike && !mapping.TypeDesc.IsArray;
16481654
var pi = member.Mapping.MemberInfo as PropertyInfo;
1649-
if (pi != null && typeof(IList).IsAssignableFrom(pi.PropertyType)
1650-
&& (pi.SetMethod == null || !pi.SetMethod.IsPublic))
1655+
1656+
// Here we have to deal with some special cases for property members. The old serializers would trip over
1657+
// private property setters generally - except in the case of list-like properties. Because lists get special
1658+
// treatment, a private setter for a list property would only be a problem if the default constructor didn't
1659+
// already create a list instance for the property. If it does create a list, then the serializer can still
1660+
// populate it. Try to emulate the old serializer behavior here.
1661+
1662+
// First, for non-list properties, private setters are always a problem.
1663+
if (!isList && pi != null && pi.SetMethod != null && !pi.SetMethod.IsPublic)
16511664
{
1652-
member.Source = (value) =>
1665+
member.Source = (value) => throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
1666+
}
1667+
1668+
// Next, for list properties, we need to handle not only the private setter case, but also the case where
1669+
// there is no setter at all. Because we need to give the default constructor a chance to create the list
1670+
// first before we make noise about not being able to set a list property.
1671+
else if (isList && pi != null && (pi.SetMethod == null || !pi.SetMethod.IsPublic))
1672+
{
1673+
var addMethod = mapping.TypeDesc.Type!.GetMethod("Add");
1674+
1675+
if (addMethod != null)
16531676
{
1654-
var getOnlyList = (IList)pi.GetValue(o)!;
1655-
if (value is IList valueList)
1677+
member.Source = (value) =>
16561678
{
1657-
foreach (var v in valueList)
1679+
var getOnlyList = pi.GetValue(o)!;
1680+
if (getOnlyList == null)
16581681
{
1659-
getOnlyList.Add(v);
1682+
// No-setter lists should just be ignored if they weren't created by constructor. Private-setter lists are the noisy exception.
1683+
if (pi.SetMethod != null && !pi.SetMethod.IsPublic)
1684+
throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
16601685
}
1661-
}
1662-
else
1663-
{
1664-
getOnlyList.Add(value);
1665-
}
1666-
};
1686+
else if (value is IEnumerable valueList)
1687+
{
1688+
foreach (var v in valueList)
1689+
{
1690+
addMethod.Invoke(getOnlyList, new object[] { v });
1691+
}
1692+
}
1693+
};
1694+
}
16671695
}
1696+
1697+
// For all other members (fields, public setter properties, etc), just carry on as normal
16681698
else
16691699
{
16701700
if (member.Mapping.Xmlns != null)
@@ -1680,6 +1710,21 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje
16801710
member.Source = (value) => setterDelegate(o, value);
16811711
}
16821712
}
1713+
1714+
// Finally, special list handling again. ANY list that we can assign/populate should be initialized with
1715+
// an empty list if it hasn't been initialized already. Even if the XML data says the list should be null.
1716+
// This is an odd legacy behavior, but it's what the old serializers did.
1717+
if (isList && member.Source != null)
1718+
{
1719+
member.EnsureCollection = (obj) =>
1720+
{
1721+
if (GetMemberValue(obj, mapping.MemberInfo!) == null)
1722+
{
1723+
var empty = ReflectionCreateObject(mapping.TypeDesc.Type!);
1724+
member.Source(empty);
1725+
}
1726+
};
1727+
}
16831728
}
16841729

16851730
if (member.Mapping.CheckSpecified == SpecifiedAccessor.ReadWrite)
@@ -1729,23 +1774,29 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje
17291774
WriteAttributes(allMembers, anyAttribute, unknownNodeAction, ref o);
17301775

17311776
Reader.MoveToElement();
1777+
17321778
if (Reader.IsEmptyElement)
17331779
{
17341780
Reader.Skip();
1735-
return o;
17361781
}
1737-
1738-
Reader.ReadStartElement();
1739-
bool IsSequenceAllMembers = IsSequence();
1740-
if (IsSequenceAllMembers)
1782+
else
17411783
{
1742-
// https://github.com/dotnet/runtime/issues/1402:
1743-
// Currently the reflection based method treat this kind of type as normal types.
1744-
// But potentially we can do some optimization for types that have ordered properties.
1745-
}
1784+
Reader.ReadStartElement();
1785+
bool IsSequenceAllMembers = IsSequence();
1786+
if (IsSequenceAllMembers)
1787+
{
1788+
// https://github.com/dotnet/runtime/issues/1402:
1789+
// Currently the reflection based method treat this kind of type as normal types.
1790+
// But potentially we can do some optimization for types that have ordered properties.
1791+
}
17461792

1747-
WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);
1793+
WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);
17481794

1795+
ReadEndElement();
1796+
}
1797+
1798+
// Empty element or not, we need to ensure all our array-like members have been initialized in the same
1799+
// way as the IL / CodeGen - based serializers.
17491800
foreach (Member member in allMembers)
17501801
{
17511802
if (member.Collection != null)
@@ -1757,9 +1808,10 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje
17571808
var setMemberValue = GetSetMemberValueDelegate(o, memberInfo.Name);
17581809
setMemberValue(o, collection);
17591810
}
1811+
1812+
member.EnsureCollection?.Invoke(o!);
17601813
}
17611814

1762-
ReadEndElement();
17631815
return o;
17641816
}
17651817
}
@@ -2031,6 +2083,7 @@ internal sealed class Member
20312083
public Action<object?>? CheckSpecifiedSource;
20322084
public Action<object>? ChoiceSource;
20332085
public Action<string, string>? XmlnsSource;
2086+
public Action<object>? EnsureCollection;
20342087

20352088
public Member(MemberMapping mapping)
20362089
{

src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Globalization;
89
using System.Reflection;
910
using System.Text;
1011
using System.Xml.Schema;
@@ -122,7 +123,7 @@ private void WriteMember(object? o, object? choiceSource, ElementAccessor[] elem
122123
}
123124
else
124125
{
125-
WriteElements(o, elements, text, choice, writeAccessors, memberTypeDesc.IsNullable);
126+
WriteElements(o, choiceSource, elements, text, choice, writeAccessors, memberTypeDesc.IsNullable);
126127
}
127128
}
128129

@@ -146,10 +147,10 @@ private void WriteArray(object o, object? choiceSource, ElementAccessor[] elemen
146147
}
147148
}
148149

149-
WriteArrayItems(elements, text, choice, o);
150+
WriteArrayItems(elements, text, choice, o, choiceSource);
150151
}
151152

152-
private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, object o)
153+
private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, object o, object? choiceSources)
153154
{
154155
var arr = o as IList;
155156

@@ -158,7 +159,8 @@ private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, Cho
158159
for (int i = 0; i < arr.Count; i++)
159160
{
160161
object? ai = arr[i];
161-
WriteElements(ai, elements, text, choice, true, true);
162+
var choiceSource = ((Array?)choiceSources)?.GetValue(i);
163+
WriteElements(ai, choiceSource, elements, text, choice, true, true);
162164
}
163165
}
164166
else
@@ -169,16 +171,18 @@ private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, Cho
169171
IEnumerator e = a.GetEnumerator();
170172
if (e != null)
171173
{
174+
int c = 0;
172175
while (e.MoveNext())
173176
{
174177
object ai = e.Current;
175-
WriteElements(ai, elements, text, choice, true, true);
178+
var choiceSource = ((Array?)choiceSources)?.GetValue(c++);
179+
WriteElements(ai, choiceSource, elements, text, choice, true, true);
176180
}
177181
}
178182
}
179183
}
180184

181-
private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, bool writeAccessors, bool isNullable)
185+
private void WriteElements(object? o, object? choiceSource, ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, bool writeAccessors, bool isNullable)
182186
{
183187
if (elements.Length == 0 && text == null)
184188
return;
@@ -216,16 +220,35 @@ private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor?
216220
}
217221
else if (choice != null)
218222
{
219-
if (o != null && o.GetType() == element.Mapping!.TypeDesc!.Type)
223+
// This looks heavy - getting names of enums in string form for comparison rather than just comparing values.
224+
// But this faithfully mimics NetFx, and is necessary to prevent confusion between different enum types.
225+
// ie EnumType.ValueX could == 1, but TotallyDifferentEnumType.ValueY could also == 1.
226+
TypeDesc td = element.Mapping!.TypeDesc!;
227+
bool enumUseReflection = choice.Mapping!.TypeDesc!.UseReflection;
228+
string enumTypeName = choice.Mapping!.TypeDesc!.FullName;
229+
string enumFullName = (enumUseReflection ? "" : enumTypeName + ".@") + FindChoiceEnumValue(element, (EnumMapping)choice.Mapping, enumUseReflection);
230+
string choiceFullName = (enumUseReflection ? "" : choiceSource!.GetType().FullName + ".@") + choiceSource!.ToString();
231+
232+
if (choiceFullName == enumFullName)
220233
{
221-
WriteElement(o, element, writeAccessors);
222-
return;
234+
// Object is either non-null, or it is allowed to be null
235+
if (o != null || (!isNullable || element.IsNullable))
236+
{
237+
// But if Object is non-null, it's got to match types
238+
if (o != null && !td.Type!.IsAssignableFrom(o!.GetType()))
239+
{
240+
throw CreateMismatchChoiceException(td.FullName, choice.MemberName!, enumFullName);
241+
}
242+
243+
WriteElement(o, element, writeAccessors);
244+
return;
245+
}
223246
}
224247
}
225248
else
226249
{
227250
TypeDesc td = element.IsUnbounded ? element.Mapping!.TypeDesc!.CreateArrayTypeDesc() : element.Mapping!.TypeDesc!;
228-
if (o!.GetType() == td.Type)
251+
if (td.Type!.IsAssignableFrom(o!.GetType()))
229252
{
230253
WriteElement(o, element, writeAccessors);
231254
return;
@@ -274,6 +297,58 @@ private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor?
274297
}
275298
}
276299

300+
private static string FindChoiceEnumValue(ElementAccessor element, EnumMapping choiceMapping, bool useReflection)
301+
{
302+
string? enumValue = null;
303+
304+
for (int i = 0; i < choiceMapping.Constants!.Length; i++)
305+
{
306+
string xmlName = choiceMapping.Constants[i].XmlName;
307+
308+
if (element.Any && element.Name.Length == 0)
309+
{
310+
if (xmlName == "##any:")
311+
{
312+
if (useReflection)
313+
enumValue = choiceMapping.Constants[i].Value.ToString(CultureInfo.InvariantCulture);
314+
else
315+
enumValue = choiceMapping.Constants[i].Name;
316+
break;
317+
}
318+
continue;
319+
}
320+
int colon = xmlName.LastIndexOf(':');
321+
string? choiceNs = colon < 0 ? choiceMapping.Namespace : xmlName.Substring(0, colon);
322+
string choiceName = colon < 0 ? xmlName : xmlName.Substring(colon + 1);
323+
324+
if (element.Name == choiceName)
325+
{
326+
if ((element.Form == XmlSchemaForm.Unqualified && string.IsNullOrEmpty(choiceNs)) || element.Namespace == choiceNs)
327+
{
328+
if (useReflection)
329+
enumValue = choiceMapping.Constants[i].Value.ToString(CultureInfo.InvariantCulture);
330+
else
331+
enumValue = choiceMapping.Constants[i].Name;
332+
break;
333+
}
334+
}
335+
}
336+
337+
if (string.IsNullOrEmpty(enumValue))
338+
{
339+
if (element.Any && element.Name.Length == 0)
340+
{
341+
// Type {0} is missing enumeration value '##any' for XmlAnyElementAttribute.
342+
throw new InvalidOperationException(SR.Format(SR.XmlChoiceMissingAnyValue, choiceMapping.TypeDesc!.FullName));
343+
}
344+
// Type {0} is missing value for '{1}'.
345+
throw new InvalidOperationException(SR.Format(SR.XmlChoiceMissingValue, choiceMapping.TypeDesc!.FullName, element.Namespace + ":" + element.Name, element.Name, element.Namespace));
346+
}
347+
if (!useReflection)
348+
CodeIdentifier.CheckValidIdentifier(enumValue);
349+
return enumValue;
350+
}
351+
277352
private void WriteText(object o, TextAccessor text)
278353
{
279354
if (text.Mapping is PrimitiveMapping primitiveMapping)
@@ -369,7 +444,7 @@ private void WriteElement(object? o, ElementAccessor element, bool writeAccessor
369444
if (o != null)
370445
{
371446
WriteStartElement(name, ns, false);
372-
WriteArrayItems(mapping.ElementsSortedByDerivation!, null, null, o);
447+
WriteArrayItems(mapping.ElementsSortedByDerivation!, null, null, o, null);
373448
WriteEndElement();
374449
}
375450
}

0 commit comments

Comments
 (0)