I have written some tests for reading an XML file and validating it against an XSD schema. My data objects are using a mix of attribute based and custom IXmlSerializable implementation and I am using the XmlSerializer to perform deserialization.
My test involves inserting an unknown element into the XML so that it does not conform to the schema. I then test if the validation event fires.
If the unknown element is placed in the XML so it's a child of one of the attribute based data classes (i.e. the properties are decorated with XmlAttribute and XmlElement attributes), then the validation fires correctly.
If however, the unknown element is placed in the XML so it's a child of one of the IXmlSerializable classes, then a System.InvalidOperationException is thrown, but the validation does still fire.
The code inside the custom collection's ReadXmlElements creates a new XmlSerializer to read in the child items, it is the Deserialize call where the InvalidOperationException is thrown.
If I place a try .. catch block around this call, it gets stuck in an endless loop. The only solution appears to be to put a try-catch block around the top-level XmlSerializer.Deserialize call (as shown in the test).
Does anyone know why the XmlSerializer is behaving in this way? Ideally I would like to try to catch the exception where it is thrown, rather than having a top-level exception handler, so there is a secondary question as to why the code gets stuck in an endless loop if a try..catch block is added into the collection class.
Here is the exception that is thrown:
System.InvalidOperationException: There is an error in XML document (13, 10). ---> System.InvalidOperationException: There is an error in XML document (13, 10). ---> System.InvalidOperationException: <UnknownElement xmlns='example'> was not expected.
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderGroup.Read1_Group()
--- End of inner exception stack trace ---
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
at XmlSerializerTest.EntityCollection~1.ReadXmlElements(XmlReader reader) in C:\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 55
at XmlSerializerTest.EntityCollection~1.ReadXml(XmlReader reader) in C:\Users\NGGMN9O\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 41
at System.Xml.Serialization.XmlSerializationReader.ReadSerializable(IXmlSerializable serializable, Boolean wrappedAny)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read2_Example(Boolean isNullable, Boolean checkType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read3_Example()
--- End of inner exception stack trace ---
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
at XmlSerializerTest.StackOverflowExample.InvalidElementInGroupTest() in C:\source\repos\XmlSerializerTest\XmlSerializerTest\XmlSerializerTest.cs:line 35
Schema.xsd
<?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>
InvalidElementInGroup.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>
<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>
The Implementation
Note: The code shown in this example is not the production code. I know that I could just use a List<T>
implementation which supports serialization without needing to implement IXmlSerializable.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace XmlSerializerTest
{
public class Example
{
public Example()
{
Groups = new Groups();
}
public Groups Groups { get; set; }
}
public class Groups : EntityCollection<Group>
{
}
public class Group : Entity, IXmlSerializable
{
private EntityCollection<DerivedEntity> entityCollection;
public Group()
{
this.entityCollection = new EntityCollection<DerivedEntity>();
}
#region IXmlSerializable Implementation
public XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
// Read the attributes
ReadXmlAttributes(reader);
// Consume the start element
bool isEmptyElement = reader.IsEmptyElement;
reader.ReadStartElement();
if (!isEmptyElement)
{
ReadXmlElements(reader);
reader.ReadEndElement();
}
}
/// <summary>
/// Reads the XML elements.
/// </summary>
/// <param name="reader">The reader.</param>
public override void ReadXmlElements(XmlReader reader)
{
// Handle the optional base class description element
base.ReadXmlElements(reader);
entityCollection.ReadXmlElements(reader);
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
#endregion
}
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;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
// Read the attributes
ReadXmlAttributes(reader);
// Consume the start element
bool isEmptyElement = reader.IsEmptyElement;
reader.ReadStartElement();
if (!isEmptyElement)
{
ReadXmlElements(reader);
reader.ReadEndElement();
}
}
public virtual void ReadXmlAttributes(XmlReader reader)
{
}
public virtual void ReadXmlElements(XmlReader reader)
{
XmlSerializer deserializer = new XmlSerializer(typeof(T), "example");
while (reader.IsStartElement())
{
T item = (T)deserializer.Deserialize(reader); // throws an InvalidOperationException if an unknown element is encountered.
if (item != null)
{
Add(item);
}
}
}
public void WriteXml(XmlWriter writer)
{
throw new NotImplementedException();
}
#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 => childEntityField.Count;
public bool IsReadOnly => ((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 => childEntityField[index];
set => childEntityField[index] = value;
}
#endregion
}
[System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]
public abstract class Entity
{
public string Description { get; set; }
public string Id { get; set; }
public string Name { get; set; }
public virtual void ReadXmlAttributes(XmlReader reader)
{
Id = reader.GetAttribute("Id");
Name = reader.GetAttribute("Name");
}
public virtual void ReadXmlElements(XmlReader reader)
{
if (reader.IsStartElement("Description"))
{
Description = reader.ReadElementContentAsString();
}
}
}
public class DerivedEntity : Entity
{
public string Parameter { get; set; }
}
}
The Test
namespace XmlSerializerTest
{
using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class StackOverflowExample
{
[TestMethod]
[DeploymentItem(@"Schema.xsd")]
[DeploymentItem(@"InvalidElementInGroup.xml")]
public void InvalidElementInGroupTest()
{
// Open the file
FileStream stream = new FileStream("InvalidElementInGroup.xml", FileMode.Open);
// Configure settings
XmlReaderSettings settings = new XmlReaderSettings();
settings.Schemas.Add(null, @"Schema.xsd");
settings.ValidationType = ValidationType.Schema;
settings.ValidationEventHandler += OnValidationEvent;
XmlSerializer xmlDeserializer = new XmlSerializer(typeof(Example), "example");
// Deserialize from the stream
stream.Position = 0;
XmlReader xmlReader = XmlReader.Create(stream, settings);
try
{
Example deserializedObject = (Example)xmlDeserializer.Deserialize(xmlReader);
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e);
}
}
private void OnValidationEvent(object sender, ValidationEventArgs e)
{
Console.WriteLine("Validation Event: " + e.Message);
}
}
}