CSharp · Console
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;
}
}