OK, what am I missing here? MSDN says the following with regard to DateTimeSerializationMode:
In versions 2.0 and later of the .Net Framework, with this property set to RoundtripDateTime objects are examined to determine whether they are in the local, UTC or an unspecified time zone, and are serialized in such a way that this information is preserved. This is the default behavior and is recommended for all new applications that do not communicate with older versions of the framework.
However:
namespace ConsoleApplication1 {
public class DateSerTest {
[XmlElement(DataType = "date")]
public DateTime Date { get; set; }
}
class Program {
static void Main(string[] args) {
DateSerTest d = new DateSerTest {
Date = DateTime.SpecifyKind(new DateTime(2009,8,18), DateTimeKind.Utc),
};
XmlSerializer ser = new XmlSerializer(typeof(DateSerTest));
using (FileStream fs = new FileStream("out.xml", FileMode.Create)) {
ser.Serialize(fs, d);
}
// out.xml will contain:
// <Date>2009-08-18</Date>
using (FileStream fs = new FileStream("out.xml", FileMode.Open)) {
DateSerTest d1 = (DateSerTest) ser.Deserialize(fs);
Console.WriteLine(d1.Date); // yields: 8/18/2009 12:00:00 AM
Console.WriteLine(d1.Date.Kind); // yields: Unspecified
}
// in.xml:
// <DateSerTest>
// <Date>2009-08-18Z</Date>
// </DateSerTest>
using (FileStream fs = new FileStream("in.xml", FileMode.Open)) {
DateSerTest d1 = (DateSerTest) ser.Deserialize(fs);
Console.WriteLine(d1.Date); // yields: 8/17/2009 8:00:00 PM
Console.WriteLine(d1.Date.Kind); // yields: Local
using (FileStream fs1 = new FileStream("out2.xml", FileMode.Create)) {
ser.Serialize(fs1, d1);
// out2.xml will contain:
// <Date>2009-08-17</Date>
}
}
Console.ReadKey();
}
}
}
So for XSD elements defined as "date" rather than "dateTime", the date is not serialized as UTC. This is a problem, because if I deserialize this XML the resulting date will be of Kind Unspecified, and any conversion to UTC (which should in fact be a no-op because the UTC-ness of the date should have been preserved during the roundtrip), will change at least the time of day, with a 50% chance of making the date yesterday, depending on whether you're east or west of Greenwich.
Shouldn't the date get written as:
<Date>2009-08-18Z</Date>
?
Indeed, if I deserialize a document that contains the above, I get a DateTime that's already been converted to Local time (I'm in New York so that's Aug 17th 20:00), and if I immediately serialize that object back to XML, I get:
<Date>2009-08-17</Date>
So, UTC was converted to Local on the way in, and the time part of that Local dropped on the way out, which will make it Unspecified on the way back in again. We've lost all knowledge of the original UTC date specification of August 18th.
Here's what the W3C says about xsd:date:
[Definition:] The ·value space· of date consists of top-open intervals of exactly one day in length on the timelines of dateTime, beginning on the beginning moment of each day (in each timezone), i.e. '00:00:00', up to but not including '24:00:00' (which is identical with '00:00:00' of the next day). For nontimezoned values, the top-open intervals disjointly cover the nontimezoned timeline, one per day. For timezoned values, the intervals begin at every minute and therefore overlap.
The fundamental problem is that if I do the following:
- Construct (or otherwise receive) a UTC DateTime value.
- Serialize to XML with a schema defining that field as xsd:date
- Deserialize that XML back to a DateTime.
- Convert the DateTime to UTC (which should have no effect since the "roundtrip" should have preserved this).
Or the following:
- Deserialize an XML document containing a UTC xsd:date object (eg. 2009-08-18Z).
- Serialize it back to a new XML document without touching it.
Either of these procedures should get me the same date I put in.
Workaround
The only way I can see so far to get the roundtrip behaviour I expect is to implement the Date property as follows, on the assumption that all xsd:date elements represent UTC:
[XmlElement(DataType = "date")]
public DateTime Date {
get { return _dt; }
set { _dt = value.Kind == DateTimeKind.Unspecified ?
DateTime.SpecifyKind(value, DateTimeKind.Utc) :
value.ToUniversalTime(); }
}