views:

72

answers:

2

I have a set of .net classes that I currently serialize and use with a bunch of other code, so the format of that xml is relatively fixed (format #1). I need to generate xml in another format (format #2) that's a pretty similar structure but not exactly the same, and am wondering the best approach for this.

For example, say these are my classes:

public class Resource 
{ 
    public string Name { get; set; }
    public string Description { get; set; }
    public string AnotherField { get; set; }
    public string AnotherField2 { get; set; }
    public Address Address1 { get; set; }
    public Address Address2 { get; set; }
    public Settings Settings { get; set; }
}
public class Address
{ 
    public string Line1 { get; set; }
    public string Line2 { get; set; }
    public string City { get; set; }
} 
// This class has custom serialization because it's sort-of a dictionary. 
// (Maybe that's no longer needed but it seemed necessary back in .net 2.0).
public class Settings : IXmlSerializable
{ 
    public string GetSetting(string settingName) { ... }
    public string SetSetting(string settingName, string value) { ... }
    public XmlSchema GetSchema() { return null; }
    public void ReadXml(XmlReader reader) 
    {
        // ... reads nested <Setting> elements and calls SetSetting() appropriately 
    }
    public void WriteXml(XmlWriter writer)
    {
        // ... writes nested <Setting> elements 
    }
} 

I normally use standard XmlSerialization and it produces great XML (format #1). Something like:

<Resource>
  <Name>The big one</Name>
  <Description>This is a really big resource</Description>
  <AnotherField1>ADVMW391</AnotherField1>
  <AnotherField2>green</AnotherField2>
  <Address1>
    <Line1>1 Park Lane</Line1>
    <Line2>Mayfair</Line2>
    <City>London</City>
  </Address1>
  <Address2>
    <Line1>11 Pentonville Rd</Line1>
    <Line2>Islington</Line2>
    <City>London</City>
  </Address2>
  <Settings>
    <Setting>
      <Name>Height</Name>
      <Value>12.4</Value>
    </Setting>
    <Setting>
      <Name>Depth</Name>
      <Value>14.1028</Value>
    </Setting>
  </Settings>
</Resource>

The new XML I want to generate (format #2) looks like the current XML, except:

  • Instead of field AnotherField and AnotherField2, these should now be represented as Settings. ie as if SetSetting() was called twice before serializing, so the values appear as new elements within .

  • Instead of fields Address1 and Address2, these should be represented as an element containing two elements. The elements should have an extra attribute or two, e.g. Position and AddressType.

e.g.

<Resource>
  <Name>The big one</Name>
  <Description>This is a really big resource</Description>
  <Addresses>
    <Address>
      <Line1>1 Park Lane</Line1>
      <Line2>Mayfair</Line2>
      <City>London</City>
      <Position>1</Position>
      <AddressType>Postal</AddressType>
    </Address>
    <Address>
      <Line1>11 Pentonville Rd</Line1>
      <Line2>Islington</Line2>
      <City>London</City>
      <Position>2</Position>
      <AddressType>Postal</AddressType>
    </Address>
  </Addresses>
  <Settings>
    <Setting>
      <Name>Height</Name>
      <Value>12.4</Value>
    </Setting>
    <Setting>
      <Name>Depth</Name>
      <Value>14.1028</Value>
    </Setting>
    <Setting>
      <Name>AnotherField</Name>
      <Value>ADVMW391</Value>
    </Setting>
    <Setting>
      <Name>AnotherField2</Name>
      <Value>green</Value>
    </Setting>
  </Settings>
</Resource>

Can I use XmlAttributeOverrides to control serialization in this way? Otherwise how should I approach it?

Bear in mind that my real classes have at least 10 times the number of fields, and there are some nested classes where I'm completely happy with the default serialization, so I'd like to avoid too much manual serialization code.

Possible Options

I can see these options:

  1. Maybe it's possible to use overrides to control serialization of just the attributes I care about?
  2. Custom serialization of the Resource class for format #2, calling the default serialization of nested classes where appropriate. Not sure how to deal with Settings as I effectively want to add settings, serialize using default, then remove the added settings.
  3. Create xml using default serialization, then manipulate the XML to make the changes I need. (ick!).

Another slight complication is that Resource from my example above actually has two subtypes, each with a couple of extra fields. The default serialization handles this nicely. Any new method would need to deal with serializing these subtypes too. This means I'm not keen on a solution that involves me making different subtypes purely for serialization purposes.

A: 

UPDATE:
As others have pointed out, my approach below can also be implemented with less effort using XmlAttributeOverrides. I'll still keep my answer posted as another way to emit 2 different XML tags for a commonly inherited property, but I recommend you to look into XmlAttributeOverrides as well.

ORIGINAL ANSWER:
If you try to solve this with a simple parent-child inheritance approach, I anticipate you quickly running into problems with the XmlSerializer.

What you should do (IMHO) is make the current class(es) into the base class(es), and set the XmlElement attribute to XmlIgnore (for the fields that you want to modify). This base class set should contain all the necessary getter/setter logic.

Fork the inheritance into 2 sets of children. One set should be a naive set, which will change the XmlIgnore to [XmlElement] (no need to specify ElementName for this set). That's all this class(es) intended to do.

The second set will inherit from the base class, and change the XmlIgnore to [XmlElement(ElementName=myNameHere)] for those same fields in question. That's all this class needs to do.

Here's a sample to illustrate what I'm talking about:

Base class:

public class OriginalClass
{
    private string m_field;

    [XmlIgnore]
    public virtual string Field 
    {
        get
        {
            return m_field;
        }
        set
        {
            m_field = value;
        }
    }
}

Child class (1):

public class ChildClass : OriginalClass
{
    public ChildClass() { }

    [XmlElement]
    public override string Field
    {
        get { return base.Field; }
        set { base.Field = value; }
    }
}

Child class (2) - the one which overrides the field name:

public class ChildClass2 : OriginalClass
{
    public ChildClass2() { }

    [XmlElement(ElementName = "NewField")]
    public override string Field
    {
        get { return base.Field; }
        set { base.Field = value; }
    }
}

Sample program:

class Program
{
    static void Main(string[] args)
    {
        ChildClass obj1 = new ChildClass();
        ChildClass2 obj2 = new ChildClass2();

        obj1.Field = "testing overridden field";
        obj2.Field = "testing overridden field (2)";


        var sw = new StreamWriter(Console.OpenStandardOutput());

        XmlSerializer xs = new XmlSerializer(typeof(ChildClass));
        xs.Serialize(sw, obj1);
        Console.WriteLine();

        XmlSerializer xs2 = new XmlSerializer(typeof(ChildClass2));
        xs2.Serialize(sw, obj2);

        Console.ReadLine();
    }
}

The XML output for ChildClass2 will read "NewField".

code4life
I think what you describe would be better implemented using XmlAttributeOverrides instead of having the subclasses. But your suggestion doesn't solve my actual problem, e.g. how to move a field from one element to another.
Rory
The only way to get the XML serializer to nest that I'm aware of is to move it into a nested element.
drachenstern
@Rory, sorry for not reading the question properly...! Looking at the Address properties, it seems like you'll need refactor the Address class as well as the Resource class - there's no way you can get the XML serializer to nest otherwise, as drachenstern mentioned. And you're right, XmlAttributeOverrides will do the job as well. I'll update my post accordingly.
code4life
Unfortunately refactoring any of the classes is out of scope - there is a lot of code that depends on them, and they need to continue to output XML in their current format #1 as well as format #2
Rory
+1  A: 

I ended up solving this problem by creating a new class that did the custom serialization of attributes that needed it, then using XmlAttributeOverrides to ensure that class was used instead of the default serialization for the attribute.

public class Resource
{ 
    ...

    // the method that actually does the serialization
    public void SerializeToFormat2Xml(XmlWriter writer)
    { 
        Format2Serializer.Serialize(writer, this);
    }

    // Cache the custom XmlSerializer. Since we're using overrides it won't be cached
    // by the runtime so if this is used frequently it'll be a big performance hit
    // and memory leak if it's not cached. See docs on XmlSerializer for more.
    static XmlSerializer _format2Serializer = null;
    static XmlSerializer Format2Serializer
    { 
        get { 
            if (_format2Serializer == null) 
            { 
                XmlAttributeOverrides overrides = new XmlAttributeOverrides();
                XmlAttributes ignore = new XmlAttributes();
                ignore.XmlIgnore = true;

                // ignore serialization of fields that will go into Settings 
                overrides.Add(typeof (Resource), "AnotherField", ignore);
                overrides.Add(typeof (Resource), "AnotherField2", ignore);

                // instead of serializing the normal Settings object, we use a custom serializer field
                overrides.Add(typeof (Resource), "Settings", ignore);
                XmlAttributes attributes = new XmlAttributes();
                attributes.XmlIgnore = false;
                attributes.XmlElements.Add(new XmlElementAttribute("Settings"));
                overrides.Add(typeof (Resource), "CustomSettingsSerializer", attributes);

                // ... do similar stuff for Addresses ... not in this example


                _format2Serializer = new XmlSerializer(typeof(Resource), overrides);
           }
           return _format2Serializer;
        }
    }

    // a property only used for custom serialization of settings
    [XmlIgnore]
    public CustomSerializeHelper CustomSettingsSerializer
    {
        get { return new CustomSerializeHelper (this, "Settings"); }
        set { } // needs setter otherwise won't be serialized!
    }

    // would have a similar property for custom serialization of addresses, 
    // defaulting to XmlIgnore.
}


public class CustomSerializeHelper : IXmlSerializable
{
    // resource to serialize
    private Resource _resource;

    // which field is being serialized. 
    private string _property;

    public CustomSerializeHelper() { } // must have a default constructor
    public CustomSerializeHelper(Resource resource, string property)
    {
        _resource = resource;  
        _property = property;  
    }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        return;
    }

    public void WriteXml(XmlWriter writer)
    {
        if (_property == "Settings")
        {
            Dictionary<string, string> customSettings = new Dictionary<string, string>();
            customSettings.Add("AnotherField", _resource.AnotherField);
            customSettings.Add("AnotherField2", _resource.AnotherField2);

            _resource.Settings.WriteXml(writer, customSettings);
        }
        if (_property == "Addresses")
        { 
            // ... similar custom serialization for Address, 
            // in that case getting a new XmlSerializer(typeof(Address)) and calling
            // Serialize(writer,Address), with override to add Position.
        }
    }


public partial class Settings
{ 
    // added this new method to Settings so it can serialize itself plus
    // some additional settings.
    public void WriteXml(XmlWriter writer, Dictionary<string, string> additionalSettingsToWrite)
    {
        WriteXml(writer);
        foreach (string key in additionalSettingsToWrite.Keys)
        {
            string value = additionalSettingsToWrite[key];
            writer.WriteStartElement("Setting");
            writer.WriteElementString("SettingType", key);
            writer.WriteElementString("SettingValue", value);
            writer.WriteEndElement();
        }
    }

}
Rory
Congrats on getting a working bit of code.
drachenstern