From ffdde1dbac089892bf72ff274a900214de3d06b4 Mon Sep 17 00:00:00 2001 From: Steve Molloy Date: Thu, 4 Jun 2026 12:10:24 -0700 Subject: [PATCH] Remove unreachable NotImplementedException for array-with-choice in reflection-based XmlSerializer (#1400) WriteArray in ReflectionXmlSerializationReader threw NotImplementedException when memberMapping.ChoiceIdentifier was non-null. That memberMapping is a freshly constructed local whose ChoiceIdentifier is always null, so the throw was unreachable dead code. The choice value for an array-typed choice element is recorded by the calling WriteElement against the owning member after WriteArray returns, so the reflection-based serializer already round-trips these types correctly. Add a regression test covering an [XmlChoiceIdentifier] member where one of the choice element types is itself an array (its element mapping is an ArrayMapping). The test runs in both the codegen and ReflectionOnly XmlSerializer test projects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReflectionXmlSerializationReader.cs | 11 ++--- .../tests/XmlSerializer/XmlSerializerTests.cs | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) 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 0c08561ba20ced..1418b7342558d8 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 @@ -1139,13 +1139,10 @@ private static bool IsWildcard(SpecialMapping mapping) Type collectionType = memberMapping.TypeDesc!.Type!; o = ReflectionCreateObject(memberMapping.TypeDesc.Type!); - if (memberMapping.ChoiceIdentifier != null) - { - // https://github.com/dotnet/runtime/issues/1400: - // To Support ArrayMapping Types Having ChoiceIdentifier - throw new NotImplementedException("memberMapping.ChoiceIdentifier != null"); - } - + // When this array is the value of a member that carries an [XmlChoiceIdentifier] + // (e.g. one of the choice element types is itself an array), the parallel choice + // value is recorded by the calling WriteElement against the owning member after + // this method returns, so no additional choice handling is needed here. var arrayMember = new Member(memberMapping); arrayMember.Collection = new CollectionMember(); arrayMember.ArraySource = arrayMember.Collection.Add; diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs index 1451e7407d8148..35a2d1a01ae9e8 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs @@ -1927,6 +1927,33 @@ public static void Xml_TypeWithArrayPropertyHavingComplexChoice() Assert.True(Enumerable.SequenceEqual(value.ManyChoices, actual.ManyChoices)); } + [Fact] + public static void Xml_TypeWithArrayLikeChoiceElement() + { + // Exercises an [XmlChoiceIdentifier] member where one of the choice element types is + // itself an array, so that element's mapping is an ArrayMapping. Deserializing such a + // value drives the reflection-based reader's array-reading path while the owning member + // carries a choice identifier. + var value = new TypeWithArrayLikeChoiceElement() + { + ManyChoices = new object[] { "hello", new int[] { 1, 2, 3 } }, + ChoiceArray = new ArrayLikeChoice[] { ArrayLikeChoice.Word, ArrayLikeChoice.Numbers } + }; + + var actual = SerializeAndDeserialize(value, WithXmlHeader("\r\n hello\r\n \r\n 1\r\n 2\r\n 3\r\n \r\n")); + + Assert.NotNull(actual); + Assert.NotNull(actual.ManyChoices); + Assert.Equal(2, actual.ManyChoices.Length); + Assert.Equal("hello", actual.ManyChoices[0]); + int[] numbers = Assert.IsType(actual.ManyChoices[1]); + Assert.Equal(new int[] { 1, 2, 3 }, numbers); + + // The [XmlIgnore] choice array is populated during deserialization to mirror, per item, + // which element each value was read from. + Assert.Equal(new ArrayLikeChoice[] { ArrayLikeChoice.Word, ArrayLikeChoice.Numbers }, actual.ChoiceArray); + } + [Fact] public static void XML_TypeWithTypeNameInXmlTypeAttribute_WithValue() { @@ -3272,3 +3299,23 @@ private static string GuessCachedName(string name) throw new ArgumentException($"Cannot guess cached field name from switch name '{name}'"); } } + +public class TypeWithArrayLikeChoiceElement +{ + // One of the choice element types (Numbers) is an array, so its element mapping is an + // ArrayMapping. Each item in ManyChoices is matched to an item in ChoiceArray. + [XmlChoiceIdentifier("ChoiceArray")] + [XmlElement("Word", typeof(string))] + [XmlElement("Numbers", typeof(int[]))] + public object[] ManyChoices; + + [XmlIgnore] + public ArrayLikeChoice[] ChoiceArray; +} + +public enum ArrayLikeChoice +{ + None, + Word, + Numbers +}