Anders Lindén
Anders Lindén

Reputation: 7323

XmlSerializer does not account for local timezone when deserializing property annotated with XmlElement with DataType time

When I deserialize a time string, using XmlSerializer.Deserialize, I expect it to take my local timezone into account so that a time string in the format

00:00:00.0000000+01:00

was parsed as 00:00, because I am in the timezone GMT+1.

Did I get that wrong?

Here is the code I am running to test xml deserialization:

using System;
using System.IO;
using System.Xml.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Testing
{
    [TestClass]
    public class FooTest
    {
        [TestMethod]
        public void Test()
        {
            var serializer = new XmlSerializer(typeof(Foo),
                new XmlRootAttribute("Foo"));

            var xml = "<Foo><TheTime>00:00:00.0000000+01:00</TheTime></Foo>";

            var stream = new MemoryStream();
            var writer = new StreamWriter(stream);
            writer.Write(xml);
            writer.Flush();
            stream.Position = 0;

            var f = (Foo) serializer.Deserialize(stream);

            Assert.AreEqual("00:00", f.TheTime.ToShortTimeString()); // actual: 01:00
        }

        [Serializable]
        public class Foo
        {
            [XmlElement(DataType = "time")]
            public DateTime TheTime { get; set; }
        }
    }
}

Upvotes: 1

Views: 463

Answers (1)

Matt Johnson-Pint
Matt Johnson-Pint

Reputation: 241693

Unfortunately, there is no built-in type that you can deserialize a xs:time value into when it includes an offset (which is optional in the XSD spec).

Instead, you'll need to define a custom type and implement the appropriate interfaces for custom serialization and deserialization. Below is a minimal TimeOffset struct that will do just that.

[XmlSchemaProvider("GetSchema")]
public struct TimeOffset : IXmlSerializable
{
    public DateTime Time { get; set; }
    public TimeSpan Offset { get; set; }

    public static XmlQualifiedName GetSchema(object xs)
    {
        return new XmlQualifiedName("time", "http://www.w3.org/2001/XMLSchema");
    }

    XmlSchema IXmlSerializable.GetSchema()
    {
        // this method isn't actually used, but is required to be implemented
        return null;
    }

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        var s = reader.NodeType == XmlNodeType.Element
            ? reader.ReadElementContentAsString()
            : reader.ReadContentAsString();

        if (!DateTimeOffset.TryParseExact(s, "HH:mm:ss.FFFFFFFzzz",
            CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto))
        {
            throw new FormatException("Invalid time format.");
        }

        this.Time = dto.DateTime;
        this.Offset = dto.Offset;
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        var dto = new DateTimeOffset(this.Time, this.Offset);
        writer.WriteString(dto.ToString("HH:mm:ss.FFFFFFFzzz", CultureInfo.InvariantCulture));
    }

    public string ToShortTimeString()
    {
        return this.Time.ToString("HH:mm", CultureInfo.InvariantCulture);
    }
}

With this defined, you can now change the type of Foo.TheTime in your code to be a TimeOffset and your test will pass. You can also remove the DataType="time" in the attribute, as it's declared in the object itself via the GetSchema method.

Upvotes: 1

Related Questions