Roundtrip XML Serialization of DateTime and xsd:date?

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), ie '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(); }
    }
    

    I opened a Connect issue and got this back from Microsoft, confirming my fears:

    We have different behaviors for handling Date, Time and DateTime values. For DateTime values, if XmlDateTimeSerializationMode is not Local the information about the kind (UTC, Local or Unspecified) is preserved. This is also true while deserializing. However, for Date and Time, they are always serialized out with the same format: (yyyy-MM-dd for Date and HH:mm:ss.fffffff.zzzzzz for Time). So the information about kind is lost on serializing and deserializing. We are opening a documentation bug on our side in order to improve the documentation about this.


    I don't see the problem you described.

    Your sample code doesn't deserialize. I added some code to deserialize, and it roundtrips as I would expect. I did not see the date move back a day, or forward a day.

    I did notice that the time portion of the d.Date field is stripped for serialization, regardless of the DateTimeKind. This seems correct to me. It doesn't make sense to me, intuitively, to either serialize a timezone with a "Date", or to convert to UTC. It would be surprising to me that if I had a Date value of 8-18-2009, and when serialized, it showed up as 8-19-2009Z. So I think the way it works now seems correct.

  • DateTime's that are serialized as xsd:dateTime include zone info.
  • DateTimes serialized as xsd:date, do not. I would also expect that with [XmlElement(DateType="time")] (xsd:time), the timezone would not be included. I didn't test this.
  • So the problem as I see it is, this behavior, which makes sense to me, isn't documented clearly, especially with the changes introduced for roundtripping. The fact that DataType="date" and DataType="time" do not convert to UTC for serialization, should be clearly stated.

    you wrote:

    and any conversion to UTC will change at least the time of day,

    But I didn't see this at all. When I convert a time that is DateTimeKind.Unspecified, to Utc, it doesn't change the time of day. It just changes the kind.

    class Program
    {
        static System.IO.MemoryStream StringToMemoryStream(string s)
        {
            byte[] a = System.Text.Encoding.ASCII.GetBytes(s);
            return new System.IO.MemoryStream(a);
        }
    
    
        static void Main(string[] args)
        {
            var settings = new System.Xml.XmlWriterSettings { OmitXmlDeclaration = true, Indent= true };
            XmlSerializerNamespaces _ns = new XmlSerializerNamespaces();
            _ns.Add( "", "" );
    
            Console.WriteLine("nDate Serialization testing...");
    
            for (int m=0; m < 2; m++)
            {
                var builder = new System.Text.StringBuilder();
    
                DateTime t = DateTime.Parse("2009-08-18T22:31:24.0019-04:00");
                DateSerTest d = new DateSerTest
                    { 
                        Date = t,
                        DateTime = t
                    };
    
                Console.WriteLine("nRound {0}", m+1);
                if (m==1)
                    d.Date = d.Date.ToUniversalTime();
    
                Console.WriteLine("d.Date {2,-11} = {0} Kind({1})", d.Date.ToString("u"), d.Date.Kind.ToString(),
                                  (m==1) ? "(converted)" : "(original)" );
                Console.WriteLine("d.DateTime         = {0} Kind({1})", d.DateTime.ToString("u"), d.DateTime.Kind.ToString());
    
                XmlSerializer ser = new XmlSerializer(typeof(DateSerTest));
    
                Console.WriteLine("nSerialize d");
                using ( var writer = System.Xml.XmlWriter.Create(builder, settings))
                {
                    ser.Serialize(writer, d, _ns);
                }
                string xml = builder.ToString();
                Console.WriteLine("{0}", xml);
    
                Console.WriteLine("nDeserialize into d2");
                System.IO.MemoryStream ms = StringToMemoryStream(xml);
                DateSerTest d2= (DateSerTest) ser.Deserialize(ms);
    
                Console.WriteLine("d2.Date    = {0} Kind({1})", d2.Date.ToString("u"), d2.Date.Kind.ToString());
                Console.WriteLine("d2.DateTime= {0} Kind({1})", d2.DateTime.ToString("u"), d2.DateTime.Kind.ToString());
    
                Console.WriteLine("nAfter SpecifyKind");
                d2.Date = DateTime.SpecifyKind(d2.Date, DateTimeKind.Utc);
                Console.WriteLine("d2.Date    = {0} Kind({1})", d2.Date.ToString("u"), d2.Date.Kind.ToString());
    
                Console.WriteLine("nRe-Serialize d2");
                builder = new System.Text.StringBuilder();
                using ( var writer = System.Xml.XmlWriter.Create(builder, settings))
                {
                    ser.Serialize(writer, d2, _ns);
                }
                xml = builder.ToString();
                Console.WriteLine("{0}", xml);
    
            }
        }
    }
    

    The results:

    
        Date Serialization testing...
    
        Round 1
        d.Date (original)  = 2009-08-18 22:31:24Z Kind(Local)
        d.DateTime         = 2009-08-18 22:31:24Z Kind(Local)
    
        Serialize d
        <DateSerTest>
          <Date>2009-08-18</Date>
          <DateTime>2009-08-18T22:31:24.0019-04:00</DateTime>
        </DateSerTest>
    
        Deserialize into d2
        d2.Date    = 2009-08-18 00:00:00Z Kind(Unspecified)
        d2.DateTime= 2009-08-18 22:31:24Z Kind(Local)
    
        After SpecifyKind
        d2.Date    = 2009-08-18 00:00:00Z Kind(Utc)
    
        Re-Serialize d2
        <DateSerTest>
          <Date>2009-08-18</Date>
          <DateTime>2009-08-18T22:31:24.0019-04:00</DateTime>
        </DateSerTest>
    
        Round 2
        d.Date (converted) = 2009-08-19 02:31:24Z Kind(Utc)
        d.DateTime         = 2009-08-18 22:31:24Z Kind(Local)
    
        Serialize d
        <DateSerTest>
          <Date>2009-08-19</Date>
          <DateTime>2009-08-18T22:31:24.0019-04:00</DateTime>
        </DateSerTest>
    
        Deserialize into d2
        d2.Date    = 2009-08-19 00:00:00Z Kind(Unspecified)
        d2.DateTime= 2009-08-18 22:31:24Z Kind(Local)
    
        After SpecifyKind
        d2.Date    = 2009-08-19 00:00:00Z Kind(Utc)
    
        Re-Serialize d2
        <DateSerTest>
          <Date>2009-08-19</Date>
          <DateTime>2009-08-18T22:31:24.0019-04:00</DateTime>
        </DateSerTest>
    
    链接地址: http://www.djcxy.com/p/8130.html

    上一篇: DateTime vs DateTimeOffset

    下一篇: DateTime和xsd:date的往返XML序列化?