Soft API contract

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 via 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 is 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>

Here, I'll skip soap nodes and write only content (body) of the message to improve readability:

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

Ok, we want to 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>

Date is 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 you can see, it 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. I know two solutions for this problem, but both of them require modification of TestClass.

Custom serialization and deserialization

The idea is based on replacing SomeDate property 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.

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 to 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, don't forget to 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

blog comments powered by Disqus