13

I'm trying to use the XmlSerializer to persist a List(T) where T is an interface. The serializer does not like interfaces. I'm curious if there is a simple way to serialize a list of heterogeneous objects easily with XmlSerializer. Here's what I'm going for:

    public interface IAnimal
    {
        int Age();
    }
    public class Dog : IAnimal
    {
        public int Age()
        {
            return 1;
        }
    }
    public class Cat : IAnimal
    {
        public int Age()
        {
            return 1;
        }
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        var animals = new List<IAnimal>
        {
            new Dog(),
            new Cat()
        };

        var x = new XmlSerializer(animals.GetType());
        var b = new StringBuilder();
        var w = XmlTextWriter.Create(b, new XmlWriterSettings { NewLineChars = "\r\n", Indent = true });
        //FAIL - cannot serialize interface. Does easy way to do this exist?
        x.Serialize(w, animals);
        var s = b.ToString();    
    }
John Saunders
  • 160,644
  • 26
  • 247
  • 397
Steve
  • 2,153
  • 4
  • 22
  • 31
  • Maybe this topic help you http://stackoverflow.com/questions/10225174/using-datacontractserializer-and-dataprotectionprovider-to-serialize-and-encryp – saramgsilva Oct 02 '12 at 00:38

5 Answers5

14

You can use XmlSerializer as well, but you need to include all the possible types that can appear in the object graph you're serializing, which limits extensibility and lowers maintainability. You can do it by using an overload of the constructor of XmlSerializer:

var x = new XmlSerializer(animals.GetType(), new Type[] { typeof(Cat), typeof(Dog) });

Also, there are several issues of note when using XmlSerializer, all of the outlined here (MSDN) - for example look under the heading 'Dynamically generated assemblies'.

Alex Paven
  • 5,539
  • 2
  • 21
  • 35
  • I did try this but XmlSerializer still failed because the 'interface cannot be serialized'. I did not know this ctor for XmlSerializer, learned something new, thanks. – Steve Sep 15 '10 at 03:36
  • 6
    @Steve, I would recommend creating an abstract class called Animal that implements IAnimal. You will then be able to serialize the list (assuming you modify the XmlSerializer constructor and also have Cat and Dog inherit the abstract class instead of IAnimal. var animals = new List(); does not need to change. – J. Mitchell Jan 28 '11 at 04:31
9

The XmlSerializer can't handle an interface because it doesn't know which types to create when deserialising. To get around this you need to handle that part of the serialization yourself by implementing the IXmlSerializable interface. This allows you to record the type so you can re-create (deserialise) it.

The ListOfIAnimal class below shows how I inherited and extended the generic list List<IAnimal> to implement the required interface. I squished up your old classes adding an extra non-interface field to each so I could see that the concrete classes were getting serialised and deserialised properly.

Compared to your code I'm just using the new type ListOfIAnimal in place of List<IAnimal>, the other changes are just a little refactoring.

Its complete code, just copy it into it's own .cs file, call the first function to step through it.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace Serialiser
{
    static class SerialiseInterface
    {
        public static void SerialiseAnimals()
        {
            String finalXml;

            // Serialize
            {
                var animals = new ListOfIAnimal{
                    new Dog() { Age = 5, Teeth = 30 },
                    new Cat() { Age = 6, Paws = 4 }
                };

                var xmlSerializer = new XmlSerializer(animals.GetType());
                var stringBuilder = new StringBuilder();
                var xmlTextWriter = XmlTextWriter.Create(stringBuilder, new XmlWriterSettings { NewLineChars = "\r\n", Indent = true });
                xmlSerializer.Serialize(xmlTextWriter, animals);
                finalXml = stringBuilder.ToString();
            }

            // Deserialise
            {
                var xmlSerializer = new XmlSerializer(typeof(ListOfIAnimal));
                var xmlReader = XmlReader.Create(new StringReader(finalXml));
                ListOfIAnimal animals = (ListOfIAnimal)xmlSerializer.Deserialize(xmlReader);
            }
        }
    }

    public class ListOfIAnimal : List<IAnimal>, IXmlSerializable
    {
        public ListOfIAnimal() : base() { }

        #region IXmlSerializable
        public System.Xml.Schema.XmlSchema GetSchema() { return null; }

        public void ReadXml(XmlReader reader)
        {
            reader.ReadStartElement("ListOfIAnimal");
            while (reader.IsStartElement("IAnimal"))
            {
                Type type = Type.GetType(reader.GetAttribute("AssemblyQualifiedName"));
                XmlSerializer serial = new XmlSerializer(type);

                reader.ReadStartElement("IAnimal");
                this.Add((IAnimal)serial.Deserialize(reader));
                reader.ReadEndElement(); //IAnimal
            }
            reader.ReadEndElement(); //ListOfIAnimal
        }

        public void WriteXml(XmlWriter writer)
        {
            foreach (IAnimal animal in this)
            {
                writer.WriteStartElement("IAnimal");
                writer.WriteAttributeString("AssemblyQualifiedName", animal.GetType().AssemblyQualifiedName);
                XmlSerializer xmlSerializer = new XmlSerializer(animal.GetType());
                xmlSerializer.Serialize(writer, animal);
                writer.WriteEndElement();
            }
        }
        #endregion
    }

    public interface IAnimal { int Age { get; set; } }
    public class Dog : IAnimal { public int Age { get; set;} public int Teeth { get; set;} }
    public class Cat : IAnimal { public int Age { get; set;} public int Paws { get; set;} }
}

I thought about leaving deserialize as an exercise for the reader, but the code would'n be very useful without it.

Stephen Turner
  • 7,125
  • 4
  • 51
  • 68
  • There's no way of doing this generically is there? For example, if Age in Dog and Cat were not just int but instead T, theres no way of doing it? – user99999991 Apr 14 '14 at 20:06
  • 1
    @user999999928 see the accepted answer of [xml-serialization-of-interface-property](http://stackoverflow.com/questions/1333864/xml-serialization-of-interface-property) – nawfal Jul 10 '14 at 14:39
  • This is great. Helped me a lot. Except one thing. Adding a Member of this Class to a whole XML structure will cause problems. Within the `ReadXML` Method you should add an `if(reader.IsStartElement("IAnimal")` before the while loop and end it after the `reader.ReadEndElement();` line. Otherwise your serialization will be ended before the whole xml serializer is finished – steak Apr 11 '16 at 09:51
3

Do you have to use XmlSerializer? This is a known issue with XmlSerializer.

You can use BinaryFormatter to save to a stream:

BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, animals);

Other alternative is to use WCF's DataContractSerializer and provide types using KnownType attribute.

Aliostad
  • 80,612
  • 21
  • 160
  • 208
  • I forgot to mention that its required to be text so I can edit manually if need be, so the binary does not work. The DataContractSerializer looks nice, but I looked around and did not see an example of serializing a list of mixed type using it. Thanks! – Steve Sep 15 '10 at 03:32
2

You can use ExtendedXmlSerializer.

var serializer = new ExtendedXmlSerializer();
var xml = serializer.Serialize(animals);

Your xml will look like:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfIAnimal>
    <Dog type="Model.Dog" />
    <Cat type="Model.Cat" />
</ArrayOfIAnimal>
0

The easy way is to add the [Serializable()] decoration to your classes and change your IList to List and see if that works.

If you use interfaces then go see webturner's answer.

D. Kermott
  • 1,613
  • 17
  • 24