@@ -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