Click here to Skip to main content
Click here to Skip to main content
Go to top

OverrideXml: control XML serialization of classes you can't change

, 16 Aug 2014
Rate this:
Please Sign up or sign in to vote.
How to control XML serialization in code, without custom attributes

Version of this article with the original syntax highlighting can be found here.

Abstract

.NET framework provides an XmlSerializer class for converting user-defined types to and from XML. The shape of the XML produced can be highly customized using attributes. This works very well, but it creates two problems:

  1. To change how the class is serialized one must change the source code and recompile. This may be not possible or not desired.
  2. There is no way to serialize a class to XML in more than one fashion. This issue is especially painful when two or more versions of serialized XML exist in the wild.

Microsoft provides a little known XmlAttributeOverrides class that allows to control XML serialization without changing source code. Unfortunately, it is quite difficult to use. OverrideXml provides an intuitive interface for changing the shape of XML without touching the source code of the serialized class.

Example

Suppose you have two classes:

public class Continent
{
    public string Name { get; set; }
    public List<Country> Countries { get; set;}
}

public class Country
{
    public string Name { get; set; }
    public string Capital { get; set; }
}

If you serialize them to XML as is, the result would be similar to this:

<Continent>
  <Name>Europe</Name>
  <Countries>
    <Country>
      <Name>France</Name>
      <Capital>Paris</Capital>
    </Country>
    <Country>
      <Name>Germany</Name>
      <Capital>Berlin</Capital>
    </Country>
    <Country>
      <Name>Spain</Name>
      <Capital>Madrid</Capital>
    </Country>
  </Countries>
</Continent>

Let's say you need the XML to look like this:

<continent name="Europe">
  <state name="France" capital="Paris" />
  <state name="Germany" capital="Berlin" />
  <state name="Spain" capital="Madrid" />
</continent>

You could do this by adding custom attributes to the class definitions and recompiling the classes:

[XmlRoot("continent")]
public class Continent
{
    [XmlAttribute("name")] public string Name { get; set; }
    [XmlElement("state")]  public List<Country> Countries { get; set;}
}

public class Country
{
    [XmlAttribute("name")]    public string Name { get; set; }
    [XmlAttribute("capital")] public string Capital { get; set; }
}

The trouble is, changing custom attributes is often not a viable option. Ability to make this change depends on many conditions, and I probably missed some:

  1. You must have access to full, compilable source code.
  2. You should be able to change it, recompile it, and deploy resulting binaries where needed.
  3. The change must not interfere with any backward compatibility requirements.
  4. If the assembly in question is strongly named, then
    • Either you should be able to recompile and redeploy all the dependencies,
    • Or you should have access to the key file and be willing to setup all necessary binding redirects.

Fortunately, you can still influence the way our classes are serialized without changing the class definitions. You can build an XmlAttributeOverrides object that contains equivalents of the required custom attributes, and passing it to the XmlSerializer:

Continent continent = /* some continent */;
var serializer = new XmlSerializer(typeof(Continent), GetOverrides());
serializer.Serialize(outputStream, continent);

Here's how you do it with OverrideXml:

XmlAttributeOverrides GetOverrides()
{
    return new OverrideXml()
        .Override<Continent>()
            .XmlRoot("continent")
            .Member("Name").XmlAttribute("name")
            .Member("Countries").XmlElement("state")
        .Override<Country>()
            .Member("Name").XmlAttribute("name")
            .Member("Capital").XmlAttribute("capital")
        .Commit();
}

Here's equivalent code that uses XmlAttributeOverrides directly. I honestly tried to make it as concise as possible:

XmlAttributeOverrides GetOverridesRaw()
{
    var overrides = new XmlAttributeOverrides();

    overrides.Add(typeof(Continent), new XmlAttributes { XmlRoot = new XmlRootAttribute("continent") });

    overrides.Add(typeof(Continent), "Name", 
        new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("name")});

    overrides.Add(typeof(Continent), "Countries", 
        new XmlAttributes { XmlElements = { new XmlElementAttribute("state") } });

    overrides.Add(typeof(Country), "Name", 
        new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("name")});

    overrides.Add(typeof(Country), "Capital", 
        new XmlAttributes { XmlAttribute = new XmlAttributeAttribute("capital")});

    return overrides;
}

As you can see, the code using OverrideXml is less verbose, and easier to use and maintain. OverrideXml was designed to avoid verbosity and ceremonial code, as much as C# syntax allows. It uses "fluent" approach in which each operation returns the original object, so the operations can be chained. Attribute overrides are built incrementally, and final Commit() creates and returns an instance of XmlAttributeOverrides. If you like to think in terms of design patterns, you may consider OverrideXml an application of the builder pattern.

Full source code of the example can be found at https://github.com/ikriv/OverrideXml/blob/master/src/Program.cs.

Complete Visual Studio solution is available at https://github.com/ikriv/OverrideXml/archive/master.zip: look inside the "src" directory.

How to use OverrideXml

OverrideXml is just one source file. It is therefore not packaged as an assembly. To add the file to your project, in Visual Studio menu go to Tools→Library Package Manager→Manage NuGet Packages for Solution and search for OverrideXml in online packages.

Alternatively, you can download the source from https://github.com/ikriv/OverrideXml/blob/master/src/OverrideXml.cs and add it to your project manually.

OverrideXml is implemented by a single class Ikriv.Xml.OverrideXml. It allows you to override XML attributes for one or more types as follows:

XmlAttributeOverrides overrides = new OverrideXml()
    .overrides for type1
    .overrides for type2(optional)
    ...
    .overrides for typeN (optional)
    .Commit();

Overrides for each type look like this:

.Override<SomeClass>() // or Override(typeof(SomeClass))
    .First attribute override for SomeClass
    .Second attribute override for SomeClass
    ...
    .Member("Member1").
        First attribute override for SomeClass.Member1
        Second attribute override for SomeClass.Member1
        ...
    .Member("Member2").
        First attribute override for SomeClass.Member2
        Second attribute override for SomeClass.Member2
        ...
    ...

Calls to Override<T> and Member("Name") don't add any attributes by themselves: they only change the context for the subsequent attribute calls. Call to Member("Name") also verifies that given member exists and is public.

Attribute override calls correspond to members defined in XmlAttributes class. The most important or ones are summarized below. OverrideXml has individual calls for most frequent cases, and a generic Attr() call that allows to specify any applicable attribute. Attr() shall be used when none of the specific calls applies.

Call Meaning
.Attr(customAttribute) Simulates specified custom attribute on type or member
.XmlAttribute() Simulates [XmlAttribute] on a member
.XmlAttribute("name") Simulates [XmlAttribute("name")] on a member
.XmlElement() Simulates [XmlElement] on a member
.XmlElement("name") Simulates [XmlElement("name")] on a member
.XmlIgnore() Simulates [XmlIgnore] on a member
.XmlIgnore(false) Cancels existing [XmlIgnore] attribute on a member
.XmlRoot("element") Simulates [XmlRoot("element")] attribute on a type

An override will cancel all conflicting attributes that may exist in the class definition. E.g. .XmlAttribute() call will make given member serialize as an attribute, even if it was decorated with [XmlElement] in the class definition.

Here's an example of overrides:

var overrides = new OverrideXml()
    .Override<Continent>()
        .Attr(new XmlRootAttribute("continent") { Namespace="http://mycompany.com/schema1" })
        .Member("Name").XmlIgnore()
        .Member("Countries")
            .XmlArray()
            .XmlArrayItem("foo")
    .Commit();

Use cases for OverrideXml

Third Party Components

Changing and recompiling third party components is usually either impossible or undesired. Also, if you do succeed in changing it, it may break the code that relies on the original way the component was serialized. Similar considerations apply to your own code whose usage you do not fully control. You cannot be sure that everyone uses the new version, and that this new version does not break any assumptions our users have made.

OverrideXml allows you to change the way third party components are serialized without changing their source. Project ThirdPartySample in the demo solution demonstrates how to customize serialization of a built-in .NET framework class AppDomainSetup.

Here's a default serialization:

<AppDomainSetup>
  <ApplicationBase>C:\dev\ThirdPartySample\bin\Debug\</ApplicationBase>
  <ConfigurationFile>C:\dev\ThirdPartySample\bin\Debug\ThirdPartySample.exe.Config</ConfigurationFile>
  <DisallowPublisherPolicy>false</DisallowPublisherPolicy>
  <DisallowBindingRedirects>false</DisallowBindingRedirects>
  <DisallowCodeDownload>false</DisallowCodeDownload>
  <DisallowApplicationBaseProbing>false</DisallowApplicationBaseProbing>
  <ApplicationName>ThirdPartySample.exe</ApplicationName>
  <LoaderOptimization>NotSpecified</LoaderOptimization>
  <SandboxInterop>false</SandboxInterop>
</AppDomainSetup>

After we apply these overides...

var overrides = new OverrideXml()
    .Override<AppDomainSetup>()
        .Member("ApplicationBase").XmlAttribute()
        .Member("ConfigurationFile").XmlAttribute()
        .Member("DisallowPublisherPolicy").XmlAttribute().XmlDefaultValue(false)
        .Member("DisallowBindingRedirects").XmlAttribute().XmlDefaultValue(false)
        .Member("DisallowCodeDownload").XmlAttribute().XmlDefaultValue(false)
        .Member("DisallowApplicationBaseProbing").XmlAttribute().XmlDefaultValue(false)
        .Member("ApplicationName").XmlAttribute()
        .Member("LoaderOptimization").XmlAttribute().XmlDefaultValue(LoaderOptimization.NotSpecified)
        .Member("SandboxInterop").XmlAttribute().XmlDefaultValue(false)
    .Commit();

... most of the default values become not necessary to serialize and others are converted to attributes. Resulting custom XML looks like this:

<AppDomainSetup 
  ApplicationBase="C:\dev\ThirdPartySample\bin\Debug\" 
  ConfigurationFile="C:\dev\ThirdPartySample\bin\Debug\ThirdPartySample.exe.Config" 
  ApplicationName="ThirdPartySample.exe" />

Full source code of this example is in the ThirdPartySample project of the solution.

Versioning

A typical versioning scenario unfolds as follows. You build version one of some class (e.g. MyData) that is serialized to externally stored XML documents. Then you build version two of MyData that is serialized to a new XML schema, but you want it to be able to read old files as well.

Of course, if you have drastically altered the structure of MyData, you would need to create a migration class that reads old files and manually converts them to instances of MyData version 2. On the other hand, if you only added new members to MyData, and they all are optional, you may be able to get backward compatibility for free: XmlSerializer would simply set new members to default values when reading old XML files.

The most irritating case is when the changes between version 1 and version 2 are minor, but the backward compatibility is lost. Typical cases include converting elements to attributes, changing XML namespace, renaming members, and the like. In this cases OverrideXml can help. Let's consider a concrete example. Suppose version 1 of your class looks as follows:

/* MyData, version 1 */
public class Organization
{
    public string Name { get; set;}
    public string Headquarters { get; set;}
}

[XmlRoot(Namespace=Names.XmlNamespace)]
public class MyData
{
    public DateTime CreationTime { get; set; }
    public Organization Organization { get; set; }
}

It serializes to XML like this:

<MyData xmlns="http://ikriv.com/OverrideXml/FileCreator/v1">
  <CreationTime>2014-08-11T01:43:34.9688402Z</CreationTime>
  <Organization>
    <Name>United Nations</Name>
    <Headquarters>New York</Headquarters>
  </Organization>
</MyData>

Then in version 2 you make a number of relatively small changes:

  • XML namespace is changed
  • MyData can now store multiple organizations instead of one
  • Organization.Headquarters is renamed to Organization.Location
  • Organization.Name and Organization.Location are now attributes, not elements
/* MyData, version 2 */
public class Organization
{
    [XmlAttribute] public string Name { get; set;}
    [XmlAttribute] public string Location { get; set;} // was Headquarters
}

[XmlRoot(Namespace=Names.XmlNamespace)]
public class MyData
{
    [XmlAttribute] public DateTime CreationTime { get; set; }
    public List<Organization> Organizations { get; set; }
}

Resulting XML looks like this:

<mydata creationtime="2014-08-12T02:52:30.649185Z" xmlns="http://ikriv.com/OverrideXml/FileCreator/v2">
  <organizations>
    <organization location="New York" name="United Nations">
    <organization location="Zurich" name="FIFA">
  </organization></organization></organizations>
</mydata>

If you want to read XMl version 1 with MyData class version 2, this can be achieved by using the following overrides:

new OverrideXml()
 .Override<MyData>()
     .Attr(new XmlRootAttribute { Namespace = Names.XmlNamespaceV1 })
     .Member("Organizations").XmlElement("Organization")
 .Override<Organization>()
     .Member("Name").XmlElement()
     .Member("Location").XmlElement("Headquarters")
 .Commit();

Full source code of this example is in the Versioning solution folder of the Visual Studio Solution.

Limitations of attribute overrides

Attribute overrides are a great tool for customizing produced XML, but their power has very specific limits. Here's a number of things attribute overrides cannot do:

  • Cannot add new member to a class. However, you can effectively remove a member using .XmlIgnore()
  • Cannot convert read-only {get;} property to read-write {get;set;} property. Read-only properties will not be serialized.
  • Cannot change type of a member. E.g. there is no way to serialize System.Version as String.
  • Cannot do value conversion. E.g. if old version stored distance in kilometers and new version stores distance in meters, there is no way to tell the serializer that 1.2345 in the source XML should be deserialized as 1234.5.
  • Cannot turn custom serialization (IXmlSerializable) on or off. I.e. there is no way to treat class that implements IXmlSerializable as if it were a "normal" class, or vice versa.

If you run into these limitations, you will have to use either manual serialization, or "conversion" classes. E.g. the answers to this StackOverflow question give two different ways to serialize System.Version to XML using a conversion class.

Still, there are many scenarios where attribute overrides are useful, and OverrideXml makes them more intuitive and easier to use than built-in .NET framework API.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ivan Krivyakov
Technical Lead Thomson Reuters
United States United States
Ivan is a hands-on software architect/technical lead working for Thomson Reuters in the New York City area. At present I am mostly building complex multi-threaded WPF application for the financial sector, but I am also interested in cloud computing, web development, mobile development, etc.
 
Please visit my web site: www.ikriv.com.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberEmile van Gerwen18-Aug-14 1:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 16 Aug 2014
Article Copyright 2014 by Ivan Krivyakov
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid