Implement IXmlSerializable in a readonly struct

By Fons Sonnemans, posted on
3769 Views

Xml serialization is really common. For a class this is done automatically, but not for your own struct. The primitive structs from Microsoft (bool, byte, short, int, long, double, decimal, etc.) don’t have this problem. The serialization and deserialization of those types are done by the XmlSerializer class. For your own struct you have to implement the IXmlSerializable interface. This isn’t really hard, but it is harder when the struct is a readonly struct. A feature which Microsoft added to C# 7.2 a few years ago. The problem is caused by the ReadXml() method of this interface. This method returns a void which means that you have to modify the fields inside the struct, which is not allowed if the struct is readonly. Luckily there is a solution using the Unsafe.AsRef() method. In this blog I will explain how to use it.

Xml Serialization

To serialize or deserialize an object you can use an XmlSerializer. The following code example shows you how to do this. It uses a StringWriter to serialize it to a string. It uses a StringReader to deserialize from a string. You can use different readers and writers like the StreamWriter and StreamReader when you want to write it to disk or the network. The next code is written in C# 10 so it uses modern features like top-level statements, nullable reference types and implicit usings.

using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

var employee = new Employee("Fons", 2000);

var xml = SerializeToXml(employee);
Console.WriteLine(xml);

xml = "<Employee><Name>Jim</Name><Salary>4000</Salary></Employee>";
employee = DeserializeFromXml<Employee>(xml);
Console.WriteLine(employee.ToString());

static string SerializeToXml<T>(T point) {
    XmlSerializer serializer = new XmlSerializer(typeof(T));
    using (var sw = new StringWriter()) {
        serializer.Serialize(sw, point);
        return sw.ToString();
    }
}

static T DeserializeFromXml<T>(string xml) {
    XmlSerializer serializer = new XmlSerializer(typeof(T));
    using (var sr = new StringReader(xml)) {
        return (T)serializer.Deserialize(sr)!;
    }
}

public class Employee {

    public string? Name { get; set; }

    public decimal Salary { get; set; }

    public Employee() {
        // Public parameterless constructuctor is
        // required for Deserialization
    }

    public Employee(string? name, decimal salary) {
        Name = name;
        Salary = salary;
    }

    public override string ToString() => $"Employee {Name} {Salary}";
}

The output of the serialization is then

<?xml version="1.0" encoding="utf-16"?>
<Employee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Name>Fons</Name>
   <Salary>2000</Salary>
</Employee>

Because the Employee is a class the serialization doesn’t require any special treatment. You can although use attributes to finetune the process. For example you can use the XmlAttribute to indicate that a property should be serialized into an xml attribute and not into an xml element. In the next example this attribute is used for the Salary property.

public class Employee {

    public string? Name { get; set; }

    [XmlAttribute]
    public decimal Salary { get; set; }

    public Employee() {
        // Public parameterless constructuctor is
        // required for Deserialization
    }

    public Employee(string? name, decimal salary) {
        Name = name;
        Salary = salary;
    }

    public override string ToString() => $"Employee {Name} {Salary}";
}

The output of the Serialization is then

<?xml version="1.0" encoding="utf-16"?>
<Employee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
          xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
          Salary="2000">
   <Name>Fons</Name>
</Employee>

Struct Serialization

If you want to serialize a struct you have to implement the IXmlSerializable interface. This interface has 3 methods which aren’t really difficult to implement. In this example I use a Point struct. It is implemented as a C# 10 record struct which will automatically implement fast equality logic. This logic you should otherwise have to write yourself. In the next example the WriteXml() writes the public fields X and Y to xml attributes. The ReadXml() read them from the xml attributes.

using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

var point = new Point(1, 2);
Console.WriteLine(point.ToString());

var xml = SerializeToXml(point);
Console.WriteLine(xml);

xml = "<Point X=\"3\" Y=\"4\" />";
point = DeserializeFromXml<Point>(xml);
Console.WriteLine(point.ToString());

public record struct Point : IXmlSerializable {

    public int X; 
    public int Y; 

    public Point(int x, int y) {
        X = x;
        Y = y;
    }

    public double Dist => Math.Sqrt((X * X) + (Y * Y));

    public override string ToString() => $"{X}:{Y}";

    public XmlSchema GetSchema() => null!;

    public void ReadXml(XmlReader reader) {
        int x = int.Parse(reader.MoveToAttribute("X") ? reader.Value : "0");
        int y = int.Parse(reader.MoveToAttribute("Y") ? reader.Value : "0");
        reader.Skip();

        this = new Point(x, y);
    }

    public void WriteXml(XmlWriter writer) {
        writer.WriteAttributeString("X", X.ToString());
        writer.WriteAttributeString("Y", Y.ToString());
    }
}

The output of the Serialization is then

<?xml version="1.0" encoding="utf-16"?>
<Point X="1" Y="2" />

Readonly Struct Serialization

If your struct is readonly you have to modify the ReadXml() method. In a readonly struct you can’t modify the fields of the struct. The Unsafe.AsRef() method allows you to modify the current value although it is readonly. It feels like a hack but libraries like NodaTime and Qowaiv use the same solution. NodaTime is written by John Skeet and if he dares to use it, I will use it too. The only difference in the code is in line 28.

using System.Runtime.CompilerServices;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

public readonly record struct Point : IXmlSerializable {

    public readonly int X;
    public readonly int Y;

    public Point(int x, int y) {
        X = x;
        Y = y;
    }

    public double Dist => Math.Sqrt((X * X) + (Y * Y));

    public override string ToString() => $"{X}:{Y}";

    public XmlSchema GetSchema() => null!;

    public void ReadXml(XmlReader reader) {
        int x = int.Parse(reader.MoveToAttribute("X") ? reader.Value : "0");
        int y = int.Parse(reader.MoveToAttribute("Y") ? reader.Value : "0");
        reader.Skip();

        // Update this using the Unsafe.AsRef() workaround 
        Unsafe.AsRef(this) = new Point(x, y);
    }

    public void WriteXml(XmlWriter writer) {
        writer.WriteAttributeString("X", X.ToString());
        writer.WriteAttributeString("Y", Y.ToString());
    }
}

If you want to use this solution in a project targeting the old .NET Framework of .NET Standard 2.0 you have to add the System.Runtime.CompilerServices.Unsafe NuGet package to the project. This will allow you to use the Unsafe class.

Closure

I hope you like this blog post. C# and .NET are great but you have to know the quirks.

Fons

Tags

CSharp

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.

Leave a comment

Blog comments

0 responses