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.