I have a legacy system that uses some data that is stored in XML format. The format of the XML cannot be changed without causing critical issues (i.e. tooling and other software that reads the format).
The XML is structured like this:
<Things>
<ThingOne Name="FirstThing">
<ThingOne Name="FirstChildThing" />
<ThingTwo Name="SecondChildThing"/>
</ThingOne>
<ThingTwo Name="SecondThing">
<ThingOne Name="ThirdChildThing" />
<ThingTwo Name="FourthChildThing" />
</ThingTwo>
</Things>
In my scenario "ThingOne" and "ThingTwo" are classes that extend "SomeThing". Like this:
public abstract class SomeThing
{
public int ItemNumber {get; set;}
public string Name {get; set;}
public List<SomeThing> ChildThings {get; set;}
public abstract void InspectChildren();
}
public sealed class ThingOne : SomeThing
{
public override void InspectChildren()
{
/*Iterates Child Things and performs actions*/
}
}
public sealed class ThingTwo : SomeThing
{
public override void InspectChildren()
{
/*Iterates Child Things and performs actions different from thing one*/
}
}
And "Things" is a class with a collection of "SomeThing":
public sealed class Things
{
/*Thing One and Thing Two should be deserialized to here*/
public List<SomeThing> ThingItems {get; set;}
}
There is a thing inspector like this:
public static ThingInspector
{
public static void InspectThings(IEnumerable<SomeThing> thingsToInspect)
{
foreach(var thing in thingsToInspect)
{
thing.InspectChildren()
}
}
}
However when I use the XmlSerializer to deserialize the XML into an instance of "Things" it doesn't pickup "ThingOne" and "ThingTwo" and add them into ThingItems.
I have tried decorating the classes as follows:
public sealed class Things:IList<SomeThing>
{
/*Thing One and Thing Two should be deserialized to here*/
[XmlArrayItem(typeof(ThingOne), ElementName = "ThingOne")]
[XmlArrayItem(typeof(ThingTwo), ElementName = "ThingTwo")]
public List<SomeThing> ThingItems {get; set;}
/*Example doesn't include IList implementation, but it is present and correct*/
}
public abstract class SomeThing
{
[XmlIgnore]
public int ItemNumber {get; set;}
[XmlAttribute]
public string Name {get; set;}
[XmlArrayItem(typeof(ThingOne), ElementName = "ThingOne")]
[XmlArrayItem(typeof(ThingTwo), ElementName = "ThingTwo")]
public List<SomeThing> ChildThings {get; set;}
public abstract void InspectChildren();
}
The deserialized object is an instance of "Things", but ThingItems is empty.
What am I missing? I have tried changing the xml format so that "ThingOne" and "ThingTwo" are wrapped in a "ThingItems" list and that works fine, but the xml format in the production system cannot be changed without causing failures with other software that is reading and writing the xml data.
My problem is that I need to deserialize the child elements of "Things" into a list on the corresponding "Things" class instead of forcing the object structure onto the XML. Any guidance would be appreciated.
You can deserialize your XML by modifying your types as follows:
public interface IHasThings
{
List<SomeThing> ChildThings { get; set; }
}
[XmlRoot("Things")]
public class Things : IHasThings
{
[XmlElement(typeof(ThingOne), ElementName = "ThingOne")]
[XmlElement(typeof(ThingTwo), ElementName = "ThingTwo")]
public List<SomeThing> ChildThings { get; set; } = new List<SomeThing>();
}
public abstract class SomeThing : IHasThings
{
[XmlIgnore]
public int ItemNumber { get; set; }
[XmlAttribute]
public string Name {get; set;}
[XmlElement(typeof(ThingOne), ElementName = "ThingOne")]
[XmlElement(typeof(ThingTwo), ElementName = "ThingTwo")]
public List<SomeThing> ChildThings {get; set;} = new List<SomeThing>();
public abstract void InspectChildren();
}
Notes:
By applying [XmlElementAttribute]
to your lists, the list elements will be serialized as immediate children of the parent's element without inserting an extra, intermediate container element.
Polymorphism is handled by applying [XmlElement(typeof(TThing), ElementName = "ThingName")]
which specifies an alternate XML element, name for that specific polymorphic child type, similarly to how XmlArrayItemAttribute(String, Type)
works for collections with a container element. XmlArrayItem
and XmlElement
should not be applied to the same property.
Introduction of IHasThings
is optional but may make it easier to traverse your hierarchy, treating the root container and child things in a uniform manner.
ThingOne
and ThingTwo
are unchanged.
A good way to debug issues in XML deserialization is to manually construct your expected deserialization graph, and then serialize it. Any divergences between the observed and expected XML likely indicate where your deserialization bug lies. With your data model you would have seen an extra level of container element <ChildThings>
for the child collections.
Demo fiddle here.