Thursday 28 October 2010

Selectively Overriding XML Serialisation Attributes

Introduction

As I mentioned in my last post, although you can override XML serialisation attributes by passing an XmlAttributeOverrides instance to an XmlSerializer, the attributes you provide for a given type and member replace all the existing XML serialisation attributes - you can't simply tweak one or two and leave the rest intact.

If you're thinking that type and members only tend to have one or two serialistion attributes, then take a look at this set attributes from an auto-generated EquityDerivativeBase class:

[System.Xml.Serialization.XmlIncludeAttribute(typeof(EquityDerivativeShortFormBase))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(EquityOptionTransactionSupplement))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(BrokerEquityOption))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(EquityDerivativeLongFormBase))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(EquityOption))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(EquityForward))]
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.1")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://www.fpml.org/FpML-5/confirmation")]

Let's suppose I want to use XmlAttributeOverrides to alter the value of the XmlTypeAttribute at run-rime, to place the element in a different namespace. Well I can. But the XmlAttributeOverrides instance I supply is used to replace all the existing attributes. So I lose each of the XmlIncludeAttribute attributes which define the classes which use this class as a base class.

Book and Genre classes (with Xml Attributes)

To demonstrate how to override the attributes selectively I'm going to use the same Book class as in my last post to demonstrate selectively overriding these attributes. I've added a lot more attributes to the members of the Book class to demonstrate that they all get retained.

[XmlType(TypeName="Book")]
[XmlRoot("book", Namespace="http://tempuri.org")]
public class Book
{
  [XmlIgnore]
  public int InternalId { get; set; }

  [XmlElement("title")]
  public string Title { get; set; }

  [DefaultValue("Anonymous")]
  [XmlArray("authors")]
  [XmlArrayItem("author")]
  public string[] Authors { get; set; }

  [XmlElement("isbn13")]
  public string Isbn13 { get; set; }
 
  [XmlText]
  public string Extract { get; set; }

  [XmlAttribute("genre")]
  public Genre Genre { get; set; }

  [XmlNamespaceDeclarations]
  public XmlSerializerNamespaces XmlNamespaces { get; set; }

  [XmlAnyAttribute]
  public XmlAttribute[] OtherAttributes { get; set; }

  [XmlAnyElement]
  public XmlElement[] OtherElements { get; set; }

  public Book()
  {
    XmlNamespaces = new XmlSerializerNamespaces();
    XmlNamespaces.Add("ns", "http://tempuri.org");
  }
}

public enum Genre
{
  [XmlEnum("unknown")]
  Unknown,
  [XmlEnum("autobiography")]
  Autobiography,
  [XmlEnum("computing-text")]
  ComputingText,
  [XmlEnum("classic")]
  Classic
}

Solution

The solution is to copy all the existing attributes into an XmlAttributeOverrides instance (modifying them as they're copied), and then apply the XmlAttributeOverrides to the XmlSerializer. That way the XmlAttributeOverrides object retains all of the original attributes (with the exception of any changes made in transit). Let me show you what I mean:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

namespace ConsoleApplication
{
  public class Program
  {
    static void Main(string[] args)
    {
      // create some Books
      Book[] books = new Book[]
      {
        new Book { InternalId=1, Title="The Road Ahead", Authors=new string[] {"Bill Gates"}, Isbn13="978-0670859139", Genre=Genre.Autobiography },
        new Book { InternalId=2, Title="Beowulf", Authors=new string[] {"Anonymous"}, Isbn13="978-1588278296", Extract="That grim spirit was called Grendel,\nFamous waste-wanderer that held the moors\nFen and fastness; the land of the race of monsters\nThe unhappy creature occupied for a while\nAfter the Creator had condemned them.", Genre=Genre.Classic },
        new Book { InternalId=3, Title="The C Programming Language (2nd Edition)", Authors=newstring[] {"Brian W Kernighan","Dennis M Ritchie"}, Isbn13="978-0131103627", Genre=Genre.ComputingText },
      };

      // copy the existing attributes into an XmlAttributeOverrides instance (providing an
      // Action which tweaks the attributes for Book.Isbn13)
      XmlAttributeOverrides xmlAttributeOverrides = GetXmlAttributeOverrides(new Type[] { typeof(Book), typeof(Genre) },
        (type, memberName, xmlAttributes) =>
        {
          if (type == typeof(Book) && memberName == "Isbn13")
          {
            // remove the sttribute which specifies an element named "isbn13"
            xmlAttributes.XmlElements.Clear();
            // and add an attribute which specifies an attribute named "isbn"
            xmlAttributes.XmlAttribute = new XmlAttributeAttribute("isbn");
          }
        });

      // serialise the books into a MemoryStream (using the overrides)
      XmlSerializer xmlSerializer = new XmlSerializer(typeof(Book[]), xmlAttributeOverrides);
      MemoryStream memoryStream = new MemoryStream();
      xmlSerializer.Serialize(memoryStream, books);

      // write the contents of the MemoryStream to the Console
      memoryStream.Position = 0L;
      StreamReader streamReader = new StreamReader(memoryStream);
      Console.WriteLine(streamReader.ReadToEnd());

      // wait for user to hit ENTER
      Console.ReadLine();
    }

    private static XmlAttributeOverrides GetXmlAttributeOverrides(Type[] types, Action<Type, string, XmlAttributes> tweakAttributesAction)
    {
      XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides();
      XmlAttributes xmlAttributes;

       foreach (Type type in types)
       {
        // get the Type's attributes first
        xmlAttributes = GetXmlAttributes(type.GetCustomAttributes(false));
        xmlAttributeOverrides.Add(type, xmlAttributes);

        // then iterate over the members, checking those attributes too
        if (type.IsEnum)
        {
          foreach (FieldInfo fieldInfo in type.GetFields(BindingFlags.Static | BindingFlags.Public))
          {
            xmlAttributes = GetXmlAttributes(fieldInfo.GetCustomAttributes(false));
            tweakAttributesAction(type, fieldInfo.Name, xmlAttributes);
            xmlAttributeOverrides.Add(type, fieldInfo.Name, xmlAttributes);
          }
        }
        else
        {
          foreach (PropertyInfo propertyInfo in type.GetProperties())
          {
            xmlAttributes = GetXmlAttributes(propertyInfo.GetCustomAttributes(false));
            tweakAttributesAction(type, propertyInfo.Name, xmlAttributes);
            xmlAttributeOverrides.Add(type, propertyInfo.Name, xmlAttributes);
          }
        }
      }

      return xmlAttributeOverrides;
    }

    private static XmlAttributes GetXmlAttributes(object[] attributes)
    {
      XmlAttributes xmlAttributes =new XmlAttributes();

      Dictionary<Type, Action<object>> dictionary = new Dictionary<Type, Action<object>>()
      {
        { typeof(XmlAnyAttributeAttribute), attribute => xmlAttributes.XmlAnyAttribute = attribute as XmlAnyAttributeAttribute },
        { typeof(XmlAnyElementAttribute), attribute => xmlAttributes.XmlAnyElements.Add(attribute as XmlAnyElementAttribute) },
        { typeof(XmlArrayAttribute), attribute => xmlAttributes.XmlArray = attribute as XmlArrayAttribute },
        { typeof(XmlArrayItemAttribute), attribute => xmlAttributes.XmlArrayItems.Add(attribute as XmlArrayItemAttribute) },
        { typeof(XmlAttributeAttribute), attribute => xmlAttributes.XmlAttribute = attribute as XmlAttributeAttribute },
        { typeof(DefaultValueAttribute), attribute => xmlAttributes.XmlDefaultValue = (attribute as DefaultValueAttribute).Value },
        { typeof(XmlElementAttribute), attribute => xmlAttributes.XmlElements.Add(attribute as XmlElementAttribute) },
        { typeof(XmlEnumAttribute), attribute => xmlAttributes.XmlEnum = attribute as XmlEnumAttribute },
        { typeof(XmlIgnoreAttribute), attribute => xmlAttributes.XmlIgnore = true },
        { typeof(XmlNamespaceDeclarationsAttribute), attribute => xmlAttributes.Xmlns = true },
        { typeof(XmlRootAttribute), attribute => xmlAttributes.XmlRoot = attribute as XmlRootAttribute },
        { typeof(XmlTextAttribute), attribute => xmlAttributes.XmlText = attribute as XmlTextAttribute },
        { typeof(XmlTypeAttribute), attribute => xmlAttributes.XmlType = attribute as XmlTypeAttribute },
      };

      foreach (object attribute in attributes)
      {
        // establish what type of attribute we're dealing with
        Type type = attribute.GetType();
        // if the attribute is one of those supported by XmlAttributes
        if (dictionary.ContainsKey(type))
        {
          // lookup the appropriate action and invoke it
          dictionary[type](attribute);
        }
      }

      return xmlAttributes;
    }
}

You'll notice that I use an Action to tweak the existing attributes prior to copying them into the XmlAttributeOverrides object, rather than simpy copying them all into the XmlAttributeOverrides object and then tweaking them there. This is because you can only add attributes to XmlAttributeOverrides - you can't remove them or modify them.

The above code produced the following output. Note that the Isbn13 property has been serialised into an attribute named isbn, contrary to what the attributes applied statically request. Also note that all other attributes have been honoured.

<?xml version="1.0"?>
<ArrayOfBook xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Book xmlns:ns="http://tempuri.org" isbn="978-0670859139" genre="autobiography">
    <ns:title>The Road Ahead</ns:title>
    <ns:authors>
      <ns:author>Bill Gates</ns:author>
    </ns:authors>
  </Book>
  <Book xmlns:ns="http://tempuri.org" isbn="978-1588278296" genre="classic">
    <ns:title>Beowulf</ns:title>
    <ns:authors>
      <ns:author>Anonymous</ns:author>
    </ns:authors>That grim spirit was called Grendel,
Famous waste-wanderer that held the moors
Fen and fastness; the land of the race of monsters
The unhappy creature occupied for a while
After the Creator had condemned them.</Book>
  <Book xmlns:ns="http://tempuri.org" isbn="978-0131103627" genre="computing-text">
    <ns:title>The C Programming Language (2nd Edition)</ns:title>
    <ns:authors>
      <ns:author>Brian W Kernighan</ns:author>
      <ns:author>Dennis M Ritchie</ns:author>
    </ns:authors>
  </Book>
</ArrayOfBook>

Summary

So, to selectively override XML serialisation attributes, simply create an XmlAttributeOverrides which contains all the attributes you want to apply - even if you have to copy most of them from the attributes applied statically.

See Also

No comments:

Post a Comment