Skip to content

Commit 8856fd0

Browse files
committed
Ensure collections are initialized to empty - even if they should be null according to the xml.
1 parent d363ee0 commit 8856fd0

File tree

1 file changed

+77
-24
lines changed

1 file changed

+77
-24
lines changed

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
@@ -1663,6 +1663,11 @@ void Wrapper(object? collection, object? collectionItems)
16631663
{
16641664
if (member.Source == null && mapping.TypeDesc.IsArrayLike && !(mapping.Elements!.Length == 1 && mapping.Elements[0].Mapping is ArrayMapping))
16651665
{
1666+
// Always create a collection for (non-array) collection-like types, even if the XML data says the collection should be null.
1667+
if (!mapping.TypeDesc.IsArray)
1668+
{
1669+
member.Collection ??= new CollectionMember();
1670+
}
16661671
member.Source = (item) =>
16671672
{
16681673
member.Collection ??= new CollectionMember();
@@ -1675,26 +1680,51 @@ void Wrapper(object? collection, object? collectionItems)
16751680

16761681
if (member.Source == null)
16771682
{
1683+
var isList = mapping.TypeDesc.IsArrayLike && !mapping.TypeDesc.IsArray;
16781684
var pi = member.Mapping.MemberInfo as PropertyInfo;
1679-
if (pi != null && typeof(IList).IsAssignableFrom(pi.PropertyType)
1680-
&& (pi.SetMethod == null || !pi.SetMethod.IsPublic))
1685+
1686+
// Here we have to deal with some special cases for property members. The old serializers would trip over
1687+
// private property setters generally - except in the case of list-like properties. Because lists get special
1688+
// treatment, a private setter for a list property would only be a problem if the default constructor didn't
1689+
// already create a list instance for the property. If it does create a list, then the serializer can still
1690+
// populate it. Try to emulate the old serializer behavior here.
1691+
1692+
// First, for non-list properties, private setters are always a problem.
1693+
if (!isList && pi != null && pi.SetMethod != null && !pi.SetMethod.IsPublic)
16811694
{
1682-
member.Source = (value) =>
1695+
member.Source = (value) => throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
1696+
}
1697+
1698+
// Next, for list properties, we need to handle not only the private setter case, but also the case where
1699+
// there is no setter at all. Because we need to give the default constructor a chance to create the list
1700+
// first before we make noise about not being able to set a list property.
1701+
else if (isList && pi != null && (pi.SetMethod == null || !pi.SetMethod.IsPublic))
1702+
{
1703+
var addMethod = mapping.TypeDesc.Type!.GetMethod("Add");
1704+
1705+
if (addMethod != null)
16831706
{
1684-
var getOnlyList = (IList)pi.GetValue(o)!;
1685-
if (value is IList valueList)
1707+
member.Source = (value) =>
16861708
{
1687-
foreach (var v in valueList)
1709+
var getOnlyList = pi.GetValue(o)!;
1710+
if (getOnlyList == null)
16881711
{
1689-
getOnlyList.Add(v);
1712+
// No-setter lists should just be ignored if they weren't created by constructor. Private-setter lists are the noisy exception.
1713+
if (pi.SetMethod != null && !pi.SetMethod.IsPublic)
1714+
throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
16901715
}
1691-
}
1692-
else
1693-
{
1694-
getOnlyList.Add(value);
1695-
}
1696-
};
1716+
else if (value is IEnumerable valueList)
1717+
{
1718+
foreach (var v in valueList)
1719+
{
1720+
addMethod.Invoke(getOnlyList, new object[] { v });
1721+
}
1722+
}
1723+
};
1724+
}
16971725
}
1726+
1727+
// For all other members (fields, public setter properties, etc), just carry on as normal
16981728
else
16991729
{
17001730
if (member.Mapping.Xmlns != null)
@@ -1710,6 +1740,21 @@ void Wrapper(object? collection, object? collectionItems)
17101740
member.Source = (value) => setterDelegate(o, value);
17111741
}
17121742
}
1743+
1744+
// Finally, special list handling again. ANY list that we can assign/populate should be initialized with
1745+
// an empty list if it hasn't been initialized already. Even if the XML data says the list should be null.
1746+
// This is an odd legacy behavior, but it's what the old serializers did.
1747+
if (isList && member.Source != null)
1748+
{
1749+
member.EnsureCollection = (obj) =>
1750+
{
1751+
if (GetMemberValue(obj, mapping.MemberInfo!) == null)
1752+
{
1753+
var empty = ReflectionCreateObject(mapping.TypeDesc.Type!);
1754+
member.Source(empty);
1755+
}
1756+
};
1757+
}
17131758
}
17141759

17151760
if (member.Mapping.CheckSpecified == SpecifiedAccessor.ReadWrite)
@@ -1763,23 +1808,29 @@ void Wrapper(object elementNameObject)
17631808
WriteAttributes(allMembers, anyAttribute, unknownNodeAction, ref o);
17641809

17651810
Reader.MoveToElement();
1811+
17661812
if (Reader.IsEmptyElement)
17671813
{
17681814
Reader.Skip();
1769-
return o;
17701815
}
1771-
1772-
Reader.ReadStartElement();
1773-
bool IsSequenceAllMembers = IsSequence();
1774-
if (IsSequenceAllMembers)
1816+
else
17751817
{
1776-
// https://github.com/dotnet/runtime/issues/1402:
1777-
// Currently the reflection based method treat this kind of type as normal types.
1778-
// But potentially we can do some optimization for types that have ordered properties.
1779-
}
1818+
Reader.ReadStartElement();
1819+
bool IsSequenceAllMembers = IsSequence();
1820+
if (IsSequenceAllMembers)
1821+
{
1822+
// https://github.com/dotnet/runtime/issues/1402:
1823+
// Currently the reflection based method treat this kind of type as normal types.
1824+
// But potentially we can do some optimization for types that have ordered properties.
1825+
}
17801826

1781-
WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);
1827+
WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);
17821828

1829+
ReadEndElement();
1830+
}
1831+
1832+
// Empty element or not, we need to ensure all our array-like members have been initialized in the same
1833+
// way as the IL / CodeGen - based serializers.
17831834
foreach (Member member in allMembers)
17841835
{
17851836
if (member.Collection != null)
@@ -1791,9 +1842,10 @@ void Wrapper(object elementNameObject)
17911842
var setMemberValue = GetSetMemberValueDelegate(o, memberInfo.Name);
17921843
setMemberValue(o, collection);
17931844
}
1845+
1846+
member.EnsureCollection?.Invoke(o!);
17941847
}
17951848

1796-
ReadEndElement();
17971849
return o;
17981850
}
17991851
}
@@ -2071,6 +2123,7 @@ internal sealed class Member
20712123
public Action<object?>? CheckSpecifiedSource;
20722124
public Action<object>? ChoiceSource;
20732125
public Action<string, string>? XmlnsSource;
2126+
public Action<object>? EnsureCollection;
20742127

20752128
public Member(MemberMapping mapping)
20762129
{

0 commit comments

Comments
 (0)