Xml serializer - volatile api contract

Published on Tuesday, December 26, 2017

Working with some API you expect that this API will stay stable and return the same type of result from call to call. But, sometimes it does not work this way. In the article I will tell about integration with SOAP-service and volatile contracts.

To describe the problem, I need to give you some context information.

Some remote service is described with WSDL. So, it looks like I only need to generate client and classes and work is done!

One of data transfer object can be simplified to this code:

public class TestClass
{
    [XmlElement(DataType = "date")]
    public DateTime SomeDate { get; set; }

    [XmlElement]
    public string Value { get; set; }
}

The problem appears in multiple usage of TestClass in different methods. For example, there is a method, where TestClass is a parameter, and another method, where it's a result:

interface IClient
{
    void SendRequest(TestClass request);

    TestClass GetData();
}

SOAP-service is based on XML messages. Example can look like:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
     <getProductDetails xmlns="http://warehouse.example.com/ws">
       <productID>12345</productID>
     </getProductDetails>
   </soap:Body>
</soap:Envelope>

To improve readability, we'll skip soap nodes and write only content (body) of the message:

<getProductDetails xmlns="http://warehouse.example.com/ws">
  <productID>12345</productID>
</getProductDetails>

Let's invoke SendRequest method and send data:

var request = new TestClass
{
    SomeDate = new DateTime(1988, 10, 15),
    Value = "value"
};

Our SOAP-client serializes it into message:

<TestClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SomeDate>1988-10-15</SomeDate>
  <Value>value</Value>
</TestClass>

Property SomeDate was serialized as yyyy-MM-dd without time part because DataType is set to date.

If we add time part and send it, we will get an error from the server. And this is correct behavior.

But the problem is in method GetData. The server sends us the following message:

<TestClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <SomeDate>1988-10-15T15:00:00</SomeDate>
  <Value>value</Value>
</TestClass>

As we see, element SomeDate contains time part! And our SOAP-client throws an exception, that it cannot deserialize the message, because it has invalid format.

So, we use two different formats for one object: send SomeDate as date only, but receive it as datetime. There are two solutions for this problem, but both of them require modification of TestClass.

Solution 1: Custom serialization and deserialization

The idea is based on replacing propery SomeDate with another of string type:

[Serializable]
public class TestClass
{
    [XmlIgnore]
    public DateTime SomeDate
    {
        get { return DateTime.Parse(SomeDateValue); }
        set { SomeDateValue = value.ToString("yyyy-MM-dd"); }
    }

    [XmlElement(ElementName = "SomeDate")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public string SomeDateValue { get; set; }

    [XmlElement]
    public string Value { get; set; }
}

We use XmlIgnore to prevent serialization of original value, and replace it with SomeDateValue that got XML name SomeDate. Getter and setter are custom, and we control whole process (of cause, you must add null-checks and another logic). Usage of TestClass does not change, but it has one additional property, which we should not change directly.

Solution 2: Custom type

When you have many types with the same problem, you'll need to add string property for every "wrong" property in every type. Better way - replace type with another, that controls serialization and deserialization.

public struct CustomDateTime : IXmlSerializable
{
    public DateTime Value { get; private set; }

    public CustomDateTime(DateTime value)
    {
        Value = value;
    }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        Value = DateTime.Parse(reader.ReadElementContentAsString());
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteString(Value.ToString("yyyy-MM-dd"));
    }
}

[Serializable]
public class TestClass
{
    [XmlElement]
    public CustomDateTime SomeDate { get; set; }

    [XmlElement]
    public string Value { get; set; }
}

Our custom date struct stores original DateTime value, but controls process of XML transformation (add null-checks again and so on). Also change usage:

var request = new TestClass
{
    SomeDate = new CustomDateTime(new DateTime(1988, 10, 15)),
    Value = "value"
};

Or add implicit cast operators:

public static implicit operator CustomDateTime(DateTime d)
{
    return new CustomDateTime(d);
}

public static implicit operator DateTime(CustomDateTime d)
{
    return d.Value;
}

var request = new TestClass
{
    SomeDate = new DateTime(1988, 10, 15),
    Value = "value"
};

Custom serialization is good for one or two properties, when you don't need to add a lot of code. Custom type allows you to control process more flexible.

Links