I've been provided an XML from an API that I can't change, but need to be able to derive the children of a list into its respective classes based on an attribute value.
<someClass>
<list>
<fruit type="banana"></fruit>
<fruit type="orange"></fruit>
<fruit type="apple">
<numWorms>1</numWorms>
</fruit>
</list>
</someClass>
As shown above, an apple
contains a unique element numWorms
that is not present in the other types orange
and banana
.
I would like to parse the XML into a List<Fruit>
, where the derived classes class Apple : Fruit
, class Banana : Fruit
and class Orange : Fruit
are assigned appropriately based on the type
attribute in the XML, so that I can also declare virtual methods and override them for each fruit.
A lot of resources simply recommend changing the XML to be serialised differently which isn't an option as I don't control the API.
What I have so far is this:
...
[XmlElement("list")]
public MyFruitList Fruit { get; set; }
...
...
public class MyFruitList : List<Fruit>, IXmlSerializable
{
public void ReadXml(XmlReader reader)
{
// TODO: This eventually needs to iterate through the entire list
reader.Read();
var type = reader.GetAttribute("type");
Fruit fruit;
switch (type)
{
case "apple":
fruit = new Apple();
break;
default:
fruit = new Fruit(); // For now. More fruit later.
break;
}
this.Add(fruit);
}
}
...
What I'm struggling with from here is whether this is the best/clearest method of achieving what I want, and how to iterate over the rest of the list. A large blocker for me is the inconsistency when debugging; when parsing using this code in 'real-time' it appears to correctly determine the type
of the first element to be banana
, but when I begin debugging and stepping slowly to determine what to do next, the same code evaluates the first element to null
, making it hard to proceed.
CodePudding user response:
I would get the collection of all Fruit types in runtime using reflection:
var fruitTypes= Assembly.GetAssembly(typeof(Fruit)).GetTypes()
.Where(t => typeof(Fruit).IsAssignableFrom(t));
and use it in the parsing method:
reader.Read();
var typeName = reader.GetAttribute("type");
var targetType = fruitTypes.FirstOrDefault(t => t.Name.ToLower() == typeName);
if(targetType==null) targetType = typeof(Fruit); //defaults to Fruit
this.Add((Fruit) Activator.CreateInstance(targetType));
CodePudding user response:
Use Xml Linq :
using System;
using System.Linq;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml.Linq;
namespace ConsoleApp2
{
class Program
{
const string FILENAME = @"c:\temp\test.xml";
static void Main(string[] args)
{
XmlSerializer serializer = new XmlSerializer(typeof(someClass));
XmlReader reader = XmlReader.Create(FILENAME);
someClass someclass = (someClass)serializer.Deserialize(reader);
}
}
public class someClass
{
[XmlElement("list")]
public MyFruitList Fruit { get; set; }
}
public class Fruit
{
}
public class Banana : Fruit
{
}
public class Orange : Fruit
{
}
public class Apple : Fruit
{
}
public class MyFruitList : IXmlSerializable
{
public List<Fruit> fruits { get; set; }
public void ReadXml(XmlReader reader)
{
XElement list = (XElement)XElement.ReadFrom(reader);
List<XElement> xFruits = list.Descendants("fruit").ToList();
foreach (XElement xFruit in xFruits)
{
string type = (string)xFruit.Attribute("type");
Fruit fruit;
switch (type)
{
case "apple":
fruit = new Apple();
break;
case "banana":
fruit = new Banana();
break;
case "orange":
fruit = new Orange();
break;
default:
fruit = new Fruit(); // For now. More fruit later.
break;
}
if (fruits == null) fruits = new List<Fruit>();
fruits.Add(fruit);
}
}
public void WriteXml(XmlWriter writer)
{
}
public XmlSchema GetSchema()
{
return (null);
}
}
}