简体   繁体   中英

XML deserialise elements into derived classes based on attribute

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.

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));
        

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);
        }

    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM