From e78931524342de0655283ed5326b19bd3cf8ca4a Mon Sep 17 00:00:00 2001 From: Steve Molloy Date: Fri, 22 May 2026 17:11:57 -0700 Subject: [PATCH] Fix reflection-based XmlSerializer to support XmlSchemaObject-derived types Fixes two related bugs exposed when deserializing XmlSchemaObject-derived types with the reflection-based XmlSerializer: 1. ReflectionXmlSerializationReader.WriteLiteralStructMethod threw NotImplementedException when the target type was assignable from XmlSchemaObject. The IL-based reader sets DecodeName = false instead, which prevents XmlConvert.DecodeName from corrupting schema names that contain colons (e.g. xs:string). This change matches that behavior. 2. The member-flushing loop unconditionally called GetSetMemberValueDelegate for all collection-type members, including read-only collection properties like XmlSchemaElement.Constraints. Calling PropertyInfo.SetValue on a property with no setter threw ArgumentException. The fix detects read-only properties (no setter) and adds items directly into the existing collection instance returned by the getter, using AddObjectsIntoTargetCollection. Fixes #1399 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReflectionXmlSerializationReader.cs | 26 +++++-- .../XmlSerializerTests.Internal.cs | 35 --------- .../XmlSerializerTests.RuntimeOnly.cs | 75 +++++++++++++++++++ 3 files changed, 94 insertions(+), 42 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..ba7ea685facfb8 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 @@ -1576,9 +1576,7 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje { if (structMapping.TypeDesc.Type != null && typeof(XmlSchemaObject).IsAssignableFrom(structMapping.TypeDesc.Type)) { - // https://github.com/dotnet/runtime/issues/1399: - // To Support Serializing XmlSchemaObject - throw new NotImplementedException(nameof(XmlSchemaObject)); + DecodeName = false; } object? o = ReflectionCreateObject(structMapping.TypeDesc.Type!)!; @@ -1812,10 +1810,24 @@ private static XmlSerializationCollectionFixupCallback GetCreateCollectionOfObje { MemberInfo[] memberInfos = o!.GetType().GetMember(member.Mapping.Name); MemberInfo memberInfo = memberInfos[0]; - object? collection = null; - SetCollectionObjectWithCollectionMember(ref collection, member.Collection, member.Mapping.TypeDesc!.Type!); - var setMemberValue = GetSetMemberValueDelegate(o, memberInfo.Name); - setMemberValue(o, collection); + + // For read-only collection properties (no setter), add items directly to the existing + // collection instance returned by the getter. This matches what the IL-based serializer does. + if (memberInfo is PropertyInfo pi && pi.GetSetMethod(nonPublic: true) == null) + { + object? existingCollection = pi.GetValue(o); + if (existingCollection != null) + { + AddObjectsIntoTargetCollection(existingCollection, member.Collection, member.Mapping.TypeDesc!.Type!); + } + } + else + { + object? collection = null; + SetCollectionObjectWithCollectionMember(ref collection, member.Collection, member.Mapping.TypeDesc!.Type!); + var setMemberValue = GetSetMemberValueDelegate(o, memberInfo.Name); + setMemberValue(o, collection); + } } member.EnsureCollection?.Invoke(o!); diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs index 733b5ae83c1952..1178edc4606fc1 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.Internal.cs @@ -76,39 +76,4 @@ public static void XML_TypeWithFieldsOrdered() Assert.Equal(value.StringField1, actual.StringField1); Assert.Equal(value.StringField2, actual.StringField2); } - - // TODO - Move this test to XmlSerializerTests.RuntimeOnly.cs once #1399 is fixed for the ReflectionOnly serializer. - [Fact] - public static void Xml_XmlSchema() - { - var expectedXml = WithXmlHeader("\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n"); - - XmlSchema schema = new XmlSchema - { - TargetNamespace = "http://example.com/my-schema", - ElementFormDefault = XmlSchemaForm.Qualified - }; - schema.Items.Add(new XmlSchemaElement - { - Name = "MyElement", - SchemaTypeName = new XmlQualifiedName("string", "http://www.w3.org/2001/XMLSchema") - }); - schema.Items.Add(new XmlSchemaGroup - { - Name = "MyGroup", - Particle = new XmlSchemaSequence - { - Items = { new XmlSchemaElement { Name = "Item1" }, new XmlSchemaElement { Name = "Item2" } } - } - }); - schema.Namespaces.Add("xsd", "http://www.w3.org/2001/XMLSchema"); - schema.Namespaces.Add("xsi", "http://www.w3.org/2001/XMLSchema-instance"); - - var actual = SerializeAndDeserialize(schema, expectedXml, () => new XmlSerializer(typeof(XmlSchema))); - - Assert.Equal(schema.TargetNamespace, actual.TargetNamespace); - Assert.Equal(schema.ElementFormDefault, actual.ElementFormDefault); - Assert.Equal(schema.Items.Count, actual.Items.Count); - } - } diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs index 5e57f747f5ce65..07419ac5c5874f 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs @@ -861,6 +861,81 @@ public static void XML_XmlSchemaWithNamespacesWriteWithNamespaceManager() schema.Write(memStream, new XmlNamespaceManager(new NameTable())); } + [Fact] + public static void Xml_XmlSchema() + { + var expectedXml = WithXmlHeader("\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n"); + + XmlSchema schema = new XmlSchema + { + TargetNamespace = "http://example.com/my-schema", + ElementFormDefault = XmlSchemaForm.Qualified + }; + schema.Items.Add(new XmlSchemaElement + { + Name = "MyElement", + SchemaTypeName = new XmlQualifiedName("string", "http://www.w3.org/2001/XMLSchema") + }); + schema.Items.Add(new XmlSchemaGroup + { + Name = "MyGroup", + Particle = new XmlSchemaSequence + { + Items = { new XmlSchemaElement { Name = "Item1" }, new XmlSchemaElement { Name = "Item2" } } + } + }); + schema.Namespaces.Add("xsd", "http://www.w3.org/2001/XMLSchema"); + schema.Namespaces.Add("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + + var actual = SerializeAndDeserialize(schema, expectedXml, () => new XmlSerializer(typeof(XmlSchema))); + + Assert.Equal(schema.TargetNamespace, actual.TargetNamespace); + Assert.Equal(schema.ElementFormDefault, actual.ElementFormDefault); + Assert.Equal(schema.Items.Count, actual.Items.Count); + } + + [Fact] + public static void XmlSchemaObject_RoundTrip() + { + // Verify that XmlSchemaObject-derived types can be deserialized with the reflection-based + // serializer. Previously a NotImplementedException was thrown during deserialization. + var element = new XmlSchemaElement + { + Name = "TestElement", + SchemaTypeName = new XmlQualifiedName("string", "http://www.w3.org/2001/XMLSchema") + }; + var serializer = new XmlSerializer(typeof(XmlSchemaElement)); + using var ms = new MemoryStream(); + serializer.Serialize(ms, element); + ms.Position = 0; + + var result = (XmlSchemaElement?)serializer.Deserialize(ms); + Assert.NotNull(result); + Assert.Equal("TestElement", result.Name); + Assert.Equal(new XmlQualifiedName("string", "http://www.w3.org/2001/XMLSchema"), result.SchemaTypeName); + } + + [Fact] + public static void XmlSchemaObject_RoundTrip_WithConstraintInReadOnlyCollection() + { + // Verify that items in read-only collection properties (like XmlSchemaElement.Constraints) + // are correctly round-tripped using the reflection-based serializer. + var element = new XmlSchemaElement { Name = "TestElement" }; + element.Constraints.Add(new XmlSchemaKey { Name = "PrimaryKey" }); + + var serializer = new XmlSerializer(typeof(XmlSchemaElement)); + using var ms = new MemoryStream(); + serializer.Serialize(ms, element); + ms.Position = 0; + + var result = (XmlSchemaElement?)serializer.Deserialize(ms); + Assert.NotNull(result); + Assert.Equal("TestElement", result.Name); + Assert.Equal(1, result.Constraints.Count); + var key = Assert.IsType(result.Constraints[0]); + Assert.Equal("PrimaryKey", key.Name); + } + [Fact] public static void Xml_XmlElementAsRoot() {