Search code examples
c#xmlserializer

How to use derived class for XML serialization?


I am converting my working XML serialization so that the model classes inherit from abstract base classes (to allow for future use of different serial formats).

My serialization as-is is working fine but when I switch to using models derived from a base class I get all kinds of exceptions so I'm unsure of how to proceed.

My class base class is:

namespace Data
{
    public abstract class Configuration
    {
        public abstract string Schema { get; set; }

        public abstract Command[] Commands { get; set; }
    }

    public abstract class Command
    {
        public abstract string Name { get; set; }
    }
}

My derived concrete class (the class that is working just fine before it was derived) is then in a child namespace:

namespace Data.Xml
{
    [Serializable()]
    [XmlType(AnonymousType = true)]
    [XmlRoot(Namespace = "", IsNullable = false)]
    public class Configuration : Data.Configuration
    {
        [XmlAttribute("noNamespaceSchemaLocation",
            Namespace = System.Xml.Schema.XmlSchema.InstanceNamespace)]
        public override string Schema { get; set; }

        [XmlArrayItem("Command", IsNullable = false)]
        public override Data.Command[] Commands { get; set; }
    }

    [Serializable()]
    public class Command : Data.Command
    {
        public override string Name { get; set; }
    }
}

I call the serializer within that child namespace like so:

public override Data.Configuration DeserializeConfig(StreamReader config)
{ 
    var cs = new XmlSerializer(typeof(Configuration),
            new Type[] { typeof(Command) });
    return (Configuration)ConfigSerializer.Deserialize(ConfigStreamReader);
}

public override string SerializeConfig(Data.Configuration c, Encoding encoding)
{
    string Output = null;
    var Stream = new MemoryStream();
    using (var Writer = new XmlTextWriter(Stream, encoding))
    {
        Writer.Formatting = Formatting.Indented;
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add("xsi", XmlSchema.InstanceNamespace);
        (new XmlSerializer(typeof(Configuration))).Serialize(Writer, c, ns);
        Output = encoding.GetString(Stream.ToArray());
    }
    Stream.Dispose();
    return Output;
}

The resulting XML should look like:

<?xml version="1.0" encoding="utf-8"?>
<Configuration 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xsi:noNamespaceSchemaLocation="SomeSchema.xsd">
  <Commands>
    <Command>
      <Name>SomeNameValue</Name>
    </Command>
  </Commands>
</Configuration>

I am seeing the following exception when attempting to instantiate the serializer (first line in DeserializeConfig() method above):

InvalidOperationException: Types 'Data.Command' and 'Data.Xml.Command' both use the XML type name, 'Command', from namespace ''. Use XML attributes to specify a unique XML name and/or namespace for the type.

I'm not really sure why the serializer is trying to create something from the base class, sure the names are the same, that's kind of the idea of derivation and namespaces etc ... How do I properly mark this up with attributes to have it de/serialize properly?

FYI: I did see several questions already on this, but the answers all seemed specific enough to the askers requirements that I could not work out how to apply the information to this, seemingly simple, scenario.

Update: I figured out how to pass included types into the serializer at instantiation instead of having to annotate the base class so I have removed that part from my question and updated the code. This outdates bruno's suggestion and my response (although the suggested question still does not apply).

Update: I attempted to separate the names in XML namespaces by adding the derived class to a namespace (i.e. adding [XmlElement(Namespace = "http://www.foo.com/data/xml")] to each property in the derived class) but this made no difference as the serializer still seems to "see" both the base and derived class together and so thinks they're both in that namespace.


Solution

  • Finally flipping figured most of this out.

    I stepped back and started with a very simple working non-derived example and worked up to what I needed.

    There were two things going on here. First the clashing type names, then the clashing property names. While I had bits of each of these right, the amount of permutations of options for structuring each when combined together had me confused.

    To prevent the abstract and derived type names from clashing when serialized I needed to make the derived class type anonymous (here using the XmlType attribute).

    To stop the property names clashing I needed to ignore both the property in the derived class and the base class. To do this without editing the base class I was missing a vital piece, XmlAttributeOverrides. I had seen this mentioned in the MSDN documentation for XmlSerializer.Serialize() but the information there was pretty minimal in explaining what it pertained to. This answer to another question led me to David Woodward's excellent explanation.

    I have yet to try any of this with a derived type list property, or with deserialization.

    Below is complete basic example of a program that outputs a string with some serialized XML on the console output:

    using System;
    using System.Text;
    using System.IO;
    using System.Xml;
    using System.Xml.Serialization;
    
    namespace Test
    {
        class Program
        {
            static void Main(string[] args)
            {
                var TestBar = new MyXml.Bar()
                {
                    Name = "John Smith",
                };
    
                Serializer s = new MyXml.Serializer();
                var TestOutput = s.Serialize(TestBar);
                Console.WriteLine(TestOutput);
            }
        }
    
        public abstract class Bar
        {
            public abstract string Name { get; set; }
        }
    
        public abstract class Serializer
        {
            public abstract string Serialize(Bar bar);
        }
    
        namespace MyXml
        {
            public class Serializer : Test.Serializer
            {
                public override string Serialize(Test.Bar bar)
                {
                    string Output = null;
                    var Stream = new MemoryStream();
                    var Encoding = new UTF8Encoding(false, true);
    
                    // Ignore the Name property in the *base* class!
                    var ao = new XmlAttributeOverrides();
                    var a = new XmlAttributes();
                    a.XmlElements.Clear(); // Clear any element attributes
                    a.XmlAttribute = null; // Remove any attribute attributes
                    a.XmlIgnore = true; // Set the ignore attribute value true
                    ao.Add(typeof(Test.Bar), "Name", a); // Set to use with Test.Bar.Name
    
                    using (var Writer = new XmlTextWriter(Stream, Encoding))
                    {
                        Writer.Formatting = Formatting.Indented;
                        var s = new XmlSerializer(typeof(Bar), ao);
                        s.Serialize(Writer, bar);
                        Output = Encoding.GetString(Stream.ToArray());
                    }
                    Stream.Dispose();
                    return Output;
                }
            }
    
            [Serializable]
            [XmlType(AnonymousType = true)] // Make type anonymous!
            [XmlRoot(IsNullable = false)]
            public class Bar : Test.Bar
            {
                [XmlIgnore] // Ignore the Name property in the *derived* class!
                public override string Name
                {
                    get => Unreverse(ReverseName);
                    set => ReverseName = Reverse(value);
                }
    
                [XmlElement("Name", IsNullable = false)]
                public string ReverseName { get; set; }
    
                private string Unreverse(string name)
                {
                    return "John Smith"; // Smith, John -> John Smith
                }
    
                private string Reverse(string name)
                {
                    return "Smith, John"; // John Smith -> Smith, John
                }
            }
        }
    }