https://stackoverflow.com/questions/60449088/why-does-xmlserializer-throws-an-exception-and-raise-a-validationevent-when-a-sc

CSharp · Console

/Program.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Xml.Serialization;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.IO;
using System.Data;
using System.Reflection;

using NUnit.Framework; //NUnit.Framework.AssertionException

    // https://stackoverflow.com/questions/60449088/why-does-xmlserializer-throws-an-exception-and-raise-a-validationevent-when-a-sc?noredirect=1#comment106985876_60449088

    public static class XmlSerializationExtensions
    {
        public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText)
        {
            //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.readxml?view=netframework-4.8#remarks
            //When this method is called, the reader is positioned on the start tag that wraps the information for your type. 
            //That is, directly on the start tag that indicates the beginning of a serialized object. 
            //When this method returns, it must have read the entire element from beginning to end, including all of its contents. 
            //Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. 
            //Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.
            reader.MoveToContent();
            if (reader.NodeType != XmlNodeType.Element)
                throw new XmlException(string.Format("Invalid NodeType {0}", reader.NodeType));
            if (reader.HasAttributes)
            {
                for (int i = 0; i < reader.AttributeCount; i++)
                {
                    reader.MoveToAttribute(i);
                    handleXmlAttribute(reader);
                }
                reader.MoveToElement(); // Moves the reader back to the element node.
            }
            if (reader.IsEmptyElement)
            {
                reader.Read();
                return;
            }
            reader.ReadStartElement(); // Advance to the first sub element of the wrapper element.
            while (reader.NodeType != XmlNodeType.EndElement)
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    using (var subReader = reader.ReadSubtree())
                    {
                        subReader.MoveToContent();
                        handleXmlElement(subReader);
                    }
                    // ReadSubtree() leaves the reader positioned ON the end of the element, so read that also.
                    reader.Read();
                }
                else if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
                {
                    var type = reader.NodeType;
                    handleXmlText(reader);
                    // Ensure that the reader was not advanced.
                    if (reader.NodeType != type)
                        throw new XmlException(string.Format("handleXmlText incorrectly advanced the reader to a new node {0}", reader.NodeType));
                    reader.Read();
                }
                else // Whitespace, comment
                {
                    // Skip() leaves the reader positioned AFTER the end of the node.
                    reader.Skip();
                }
            }
            // Move past the end of the wrapper element
            reader.ReadEndElement();
        }

        public static void WriteIXmlSerializable(XmlWriter writer, Action<XmlWriter> writeAttributes, Action<XmlWriter> writeNodes)
        {
            //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.writexml?view=netframework-4.8#remarks
            //The WriteXml implementation you provide should write out the XML representation of the object. 
            //The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. 
            //The framework then closes the wrapper element.
            writeAttributes(writer);
            writeNodes(writer);
        }
    }

    public class Constants
    {
        public const string ExampleNamespace = "example";
    }

    [XmlRoot(Namespace = Constants.ExampleNamespace)]
    public class Example
    {
        public Example()
        {
            Groups = new Groups();
        }

        public Groups Groups { get; set; }
    }

    public class Groups : EntityCollection<Group>
    {

    }

    public class EntityCollection<T> : IXmlSerializable, IList<T> where T : Entity
    {
        private List<T> childEntityField;

        public EntityCollection()
        {
            childEntityField = new List<T>();
        }

        #region IXmlSerializable Implementation

        public XmlSchema GetSchema() { return null; }

        protected internal virtual bool HandleXmlAttribute(XmlReader reader) { return false; }

        protected internal virtual void WriteAttributes(XmlWriter writer) { }

        protected internal virtual bool HandleXmlElement(XmlReader reader)
        {
            var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);
            if (serializer.CanDeserialize(reader))
            {
                T item = (T)serializer.Deserialize(reader);
                if (item != null)
                    Add(item);
                return true;
            }
            return false;
        }

        protected internal virtual void WriteNodes(XmlWriter writer)
        {
            var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);
            foreach (var item in this)
            {
                serializer.Serialize(writer, item);
            }
        }

        public void ReadXml(XmlReader reader)
        {
            XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);
        }

        public void WriteXml(XmlWriter writer)
        {
            XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));
        }

        #endregion

        #region IList Implementation

        public IEnumerator<T> GetEnumerator()
        {
            return childEntityField.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)childEntityField).GetEnumerator();
        }

        public void Add(T item)
        {
            childEntityField.Add(item);
        }

        public void Clear()
        {
            childEntityField.Clear();
        }

        public bool Contains(T item)
        {
            return childEntityField.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            childEntityField.CopyTo(array, arrayIndex);
        }

        public bool Remove(T item)
        {
            return childEntityField.Remove(item);
        }

        public int Count { get { return childEntityField.Count; } }

        public bool IsReadOnly { get { return ((ICollection<T>)childEntityField).IsReadOnly; } }

        public int IndexOf(T item)
        {
            return childEntityField.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            childEntityField.Insert(index, item);
        }

        public void RemoveAt(int index)
        {
            childEntityField.RemoveAt(index);
        }

        public T this[int index]
        {
            get { return childEntityField[index]; }
            set { childEntityField[index] = value; }
        }

        #endregion
    }

    public class Group : Entity, IXmlSerializable
    {
        private EntityCollection<DerivedEntity> entityCollection;

        public Group()
        {
            this.entityCollection = new EntityCollection<DerivedEntity>();
        }

        #region IXmlSerializable Implementation

        public XmlSchema GetSchema()
        {
            return null;
        }

        protected override bool HandleXmlElement(XmlReader reader)
        {
            if (base.HandleXmlElement(reader))
                return true;
            return entityCollection.HandleXmlElement(reader);
        }

        protected override void WriteNodes(XmlWriter writer)
        {
            base.WriteNodes(writer);
            entityCollection.WriteNodes(writer);
        }

        protected override bool HandleXmlAttribute(XmlReader reader)
        {
            if (base.HandleXmlAttribute(reader))
                return true;
            if (entityCollection.HandleXmlAttribute(reader))
                return true;
            return false;
        }

        protected override void WriteAttributes(XmlWriter writer)
        {
            base.WriteAttributes(writer);
            entityCollection.WriteAttributes(writer);
        }

        public void ReadXml(XmlReader reader)
        {
            XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);
        }

        public void WriteXml(XmlWriter writer)
        {
            XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));
        }

        #endregion
    }

    public class DerivedEntity : Entity
    {
        [XmlAttribute]
        public string Parameter { get; set; }
    }

    [System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]
    public abstract class Entity
    {
        [XmlElement]
        public string Description { get; set; }

        [XmlAttribute]
        public string Id { get; set; }

        [XmlAttribute]
        public string Name { get; set; }

        protected virtual void WriteAttributes(XmlWriter writer)
        {
            if (Id != null)
                writer.WriteAttributeString("Id", Id);
            if (Name != null)
                writer.WriteAttributeString("Name", Name);
        }

        protected virtual bool HandleXmlAttribute(XmlReader reader)
        {
            if (reader.LocalName == "Id")
            {
                Id = reader.Value;
                return true;
            }
            else if (reader.LocalName == "Name")
            {
                Name = reader.Value;
                return true;
            }
            return false;
        }

        protected virtual void WriteNodes(XmlWriter writer)
        {
            if (Description != null)
            {
                writer.WriteElementString("Description", Description);
            }
        }

        protected virtual bool HandleXmlElement(XmlReader reader)
        {
            if (reader.LocalName == "Description")
            {
                Description = reader.ReadElementContentAsString();
                return true;
            }
            return false;
        }
    }

    public class StackOverflowExample
    {
        public void InvalidElementInGroupTest()
        {
            foreach (var xml in GetXml())
            {
                using (var schema = XmlReader.Create(new StringReader(GetXsd())))
                using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
                {
					Console.WriteLine("\nTesting deserialization...");
					
                    int errorCount = 0;
                    // Configure settings
                    var settings = new XmlReaderSettings();
                    settings.Schemas.Add(null, schema);
                    settings.ValidationType = ValidationType.Schema;
                    settings.ValidationEventHandler += (o, e) =>
                        {
                            Console.WriteLine("Validation Event: " + e.Message);
                            errorCount++;
                        };
                    XmlSerializer xmlDeserializer = new XmlSerializer(typeof(Example), "example");

                    // Deserialize from the stream
                    using (var xmlReader = XmlReader.Create(stream, settings))
                    {
                        var deserializedObject = (Example)xmlDeserializer.Deserialize(xmlReader);
                        var reserializedXml = deserializedObject.GetXml();
						Console.WriteLine("Deserialized successfully with {0} validation events.  Re-serialized XML:", errorCount);
                        Console.WriteLine(reserializedXml);

                        var inXmlCleaned = XElement.Parse(xml);
                        inXmlCleaned.Descendants().Where(e => e.Name.LocalName == "UnknownElement").Remove();

                        XmlAssert.AreEqual(inXmlCleaned, XElement.Parse(reserializedXml));
                        Assert.IsTrue(errorCount == 1);
                    }
                }
            }
        }

        static IEnumerable<string> GetXml()
        {
            // Test with and without formatting
            var xml = @"<?xml version=""1.0""?>
<Example xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns=""example"">
    <Groups>
<!-- A comment to mess things up. -->
        <Group Name=""abc"" Id=""123"">
            <DerivedEntity Id=""123"" Name=""xyz"" Parameter=""ijk"">
                <Description>def</Description>
            </DerivedEntity>
            <DerivedEntity Id=""234"" Name=""bob"" Parameter=""12""/>
        </Group>
        <Group Name=""def"" Id=""124"">
            <Description>This is a description.</Description>
        </Group>
        <UnknownElement/>
    </Groups>
</Example>";
            return new[] 
            {
                xml,
                XElement.Parse(xml).ToString(SaveOptions.DisableFormatting),
            };
        }

        static string GetXsd()
        {
            return @"<?xml version=""1.0"" encoding=""utf-8"" ?>
<xs:schema xmlns:local=""example""
           attributeFormDefault=""unqualified""
           elementFormDefault=""qualified""
           targetNamespace=""example""
           version=""1.0""
           xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
  <!--  Attribute Groups -->
  <xs:attributeGroup name=""Identifiers"">
    <xs:attribute name=""Id""
                  type=""xs:string""
                  use=""required"" />
    <xs:attribute name=""Name""
                  type=""xs:string""
                  use=""required"" />
  </xs:attributeGroup>
  <!-- Complex Types -->
  <xs:complexType abstract=""true""
                  name=""Entity"">
    <xs:sequence>
      <xs:element name=""Description""
                  type=""xs:string""
                  minOccurs=""0""
                  maxOccurs=""1"" />
    </xs:sequence>
    <xs:attributeGroup ref=""local:Identifiers"" />
  </xs:complexType>
  <xs:complexType name=""DerivedEntity"">
    <xs:complexContent>
      <xs:extension base=""local:Entity"">
        <xs:attribute name=""Parameter""
                      use=""required"" />
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
  <xs:complexType name=""Groups"">
      <xs:sequence>
          <xs:element name=""Group"" type=""local:Group"" minOccurs=""0"" maxOccurs=""unbounded""/>
      </xs:sequence>
  </xs:complexType>
  <xs:complexType name=""Group"">
    <xs:complexContent>
      <xs:extension base=""local:Entity"">
        <xs:sequence>
          <xs:element name=""DerivedEntity""
                      type=""local:DerivedEntity""
                      minOccurs=""0""
                      maxOccurs=""unbounded"" />
        </xs:sequence>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
  <!-- Main Schema Definition -->
  <xs:element name=""Example"">
      <xs:complexType>
          <xs:sequence>
              <xs:element name=""Groups""
                          type=""local:Groups""
                          minOccurs=""1""
                          maxOccurs=""1"" />
          </xs:sequence>
      </xs:complexType>
  </xs:element>
</xs:schema>";
        }
    }

    class TestClass
    {
        public static void Test()
        {
            new StackOverflowExample().InvalidElementInGroupTest();
        }
    }

    public static class XmlSerializationHelper
    {
        public static T LoadFromXml<T>(this string xmlString, XmlSerializer serial = null)
        {
            serial = serial ?? new XmlSerializer(typeof(T));
            using (var reader = new StringReader(xmlString))
            {
				return (T)serial.Deserialize(reader);
            }
        }

        public static string GetXml<T>(this T obj, XmlSerializer serializer = null, bool omitStandardNamespaces = false)
        {
            XmlSerializerNamespaces ns = null;
            if (omitStandardNamespaces)
            {
                ns = new XmlSerializerNamespaces();
                ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
            }			
            using (var textWriter = new StringWriter())
            {
                var settings = new XmlWriterSettings() { Indent = true }; // For cosmetic purposes.
                using (var xmlWriter = XmlWriter.Create(textWriter, settings))
                    (serializer ?? new XmlSerializer(obj.GetType())).Serialize(xmlWriter, obj, ns);
                return textWriter.ToString();
            }
        }
    }
				
    public static class XmlAssert
    {
        // https://weblogs.asp.net/marianor/easy-way-to-compare-two-xmls-for-equality
        // https://stackoverflow.com/questions/7318157/best-way-to-compare-xelement-objects
        // Adapted from the answer https://stackoverflow.com/a/14463137 by https://stackoverflow.com/users/1102100/robjohnson
        public static void AreEqual(
            XElement expected,
            XElement actual)
        {
            Assert.IsTrue(XNode.DeepEquals(Normalize(expected), Normalize(actual)));
        }

        private static XElement Normalize(XElement element)
        {
            if (element.HasElements)
            {
                return new XElement(
                    element.Name,
                    element.Attributes()
                           .OrderBy(a => a.Name.ToString()),
                    element.Elements()
                           .OrderBy(a => a.Name.ToString())
                           .Select(e => Normalize(e)));

            }

            if (element.IsEmpty)
            {
                return new XElement(
                    element.Name,
					"",
                    element.Attributes()
                           .OrderBy(a => a.Name.ToString()));
            }

            return new XElement(element.Name, element.Attributes()
                .OrderBy(a => a.Name.ToString()), element.Value);
        }
    }
						
	public class Program
	{
		public static void Main()
		{
			Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
			Console.WriteLine();

			try
			{
				TestClass.Test();
			}
			catch (Exception ex)
			{
				Console.WriteLine("Failed with unhandled exception: ");
				Console.WriteLine(ex);
				throw;
			}
		}
		
		public static string GetNetCoreVersion()
		{
			//https://techblog.dorogin.com/how-to-detect-net-core-version-in-runtime-ecd65ad695be
			//
			var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
			var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
			int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
			if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
				return assemblyPath[netCoreAppIndex + 1];
			return null;
		}
	}
;