30

The DateTimeOffset property I have in this class doesn't get rendered when the data is represented as Xml. What do I need to do to tell the Xml serialization to render that proper as a DateTime or DateTimeOffset?

[XmlRoot("playersConnected")]
public class PlayersConnectedViewData
{
    [XmlElement("playerConnected")]
    public PlayersConnectedItem[] playersConnected { get; set; }
}

[XmlRoot("playersConnected")]
public class PlayersConnectedItem
{
    public string name { get; set; }
    public DateTimeOffset connectedOn { get; set; }  // <-- This property fails.
    public string server { get; set; }
    public string gameType { get; set; }
}

and some sample data...

<?xml version="1.0" encoding="utf-8"?>
<playersConnected 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <playerConnected>
    <name>jollyroger1000</name>
    <connectedOn />
    <server>log1</server>
    <gameType>Battlefield 2</gameType>
  </playerConnected>
</playersConnected>

Update

I'm hoping there might be a way through an Attribute which I can decorate on the property...

Bonus Question

Any way to get rid of those two namespaces declared in the root node? Should I?

John Saunders
  • 160,644
  • 26
  • 247
  • 397
Pure.Krome
  • 84,693
  • 113
  • 396
  • 647

7 Answers7

40

It's a few years late, but here's the quick and easy way to completely serialize DateTimeOffset using ISO 8601:

[XmlElement("lastUpdatedTime")]
public string lastUpdatedTimeForXml // format: 2011-11-11T15:05:46.4733406+01:00
{
   get { return lastUpdatedTime.ToString("o"); } // o = yyyy-MM-ddTHH:mm:ss.fffffffzzz
   set { lastUpdatedTime = DateTimeOffset.Parse(value); } 
}
[XmlIgnore] 
public DateTimeOffset lastUpdatedTime;
Peter
  • 5,608
  • 1
  • 24
  • 43
  • 2
    Same with XmlConvert.ToString(DateTimeOffset) and XmlConvert.ToDateTimeOffset(String) is cleaner. But after a while it gets tedious to add so many properties for every globalized date type you want to use. So for heavy use I feel maybe creating a new struct implementing IXmlSerializable plus IConvertable and assignment operators would be better, in those cases. – Tony Wall Apr 16 '14 at 15:49
  • 1
    You can just do `return lastUpdatedTime.ToString("o")` (it emits [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).) See https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#the-round-trip-o-o-format-specifier – Daniel Liuzzi Apr 02 '18 at 14:45
17

I came up with this struct which implements XML serialization based on ISO 8601 formatting (e.g. 2011-11-11T15:05:46.4733406+01:00). Hint: Trying to parse a DateTime value such as 2011-11-11T15:05:46 fails as expected.

Feedback welcome. I didn't include the unit tests here because that would be too much text.

/// <remarks>
/// The default value is <c>DateTimeOffset.MinValue</c>. This is a value
/// type and has the same hash code as <c>DateTimeOffset</c>! Implicit
/// assignment from <c>DateTime</c> is neither implemented nor desirable!
/// </remarks>
public struct Iso8601SerializableDateTimeOffset : IXmlSerializable
{
    private DateTimeOffset value;

    public Iso8601SerializableDateTimeOffset(DateTimeOffset value)
    {
        this.value = value;
    }

    public static implicit operator Iso8601SerializableDateTimeOffset(DateTimeOffset value)
    {
        return new Iso8601SerializableDateTimeOffset(value);
    }

    public static implicit operator DateTimeOffset(Iso8601SerializableDateTimeOffset instance)
    {
        return instance.value;
    }

    public static bool operator ==(Iso8601SerializableDateTimeOffset a, Iso8601SerializableDateTimeOffset b)
    {
        return a.value == b.value;
    }

    public static bool operator !=(Iso8601SerializableDateTimeOffset a, Iso8601SerializableDateTimeOffset b)
    {
        return a.value != b.value;
    }

    public static bool operator <(Iso8601SerializableDateTimeOffset a, Iso8601SerializableDateTimeOffset b)
    {
        return a.value < b.value;
    }

    public static bool operator >(Iso8601SerializableDateTimeOffset a, Iso8601SerializableDateTimeOffset b)
    {
        return a.value > b.value;
    }

    public override bool Equals(object o)
    {
        if(o is Iso8601SerializableDateTimeOffset)
            return value.Equals(((Iso8601SerializableDateTimeOffset)o).value);
        else if(o is DateTimeOffset)
            return value.Equals((DateTimeOffset)o);
        else
            return false;
    }

    public override int GetHashCode()
    {
        return value.GetHashCode();
    }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        var text = reader.ReadElementString();
        value = DateTimeOffset.ParseExact(text, format: "o", formatProvider: null);
    }

    public override string ToString()
    {
        return value.ToString(format: "o");
    }

    public string ToString(string format)
    {
        return value.ToString(format);
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteString(value.ToString(format: "o"));
    }
}
AndiDog
  • 68,631
  • 21
  • 159
  • 205
5

I'm also unsure of the best way, but here is what I did:

[XmlElement("lastUpdatedTime")]
public string lastUpdatedTimeForXml
{
  get { return lastUpdatedTime.ToString(); }
  set { lastUpdatedTime = DateTimeOffset.Parse(value); }
}
[XmlIgnore] 
public DateTimeOffset lastUpdatedTime;
Mike
  • 51
  • 1
  • 2
  • 2
    I think your getter should be: get { return lastUpdatedTime.ToString("o"); }. This returns the string in XML date/time format. – bart Feb 22 '13 at 03:45
  • 1
    This **will** break if the xml is written and then read on workstations with different regional settings. The `DateTimeOffset.ToString()` function is for display, not for serialization. You need to specify a formatter which doesn't rely on regional settings, such as `"yyyy-MM-ddTHH\:mm\:ss.fffffffzzz"`->`2011-11-11T15:05:46.4733406+01:00` – Peter Mar 05 '14 at 08:19
  • This does not work. A quick test shows it `var now = DateTimeOffset.UtcNow;` `var nowSerialized = now.ToString();` `var nowDeserialized = DateTimeOffset.Parse(nowSerialized);` `Assert.AreNotEqual(now, nowDeserialized);` – marsop Nov 17 '16 at 07:05
4

I have found the solution here: http://tneustaedter.blogspot.com/2012/02/proper-way-to-serialize-and-deserialize.html

Replacing XmlSerializer with DataContractSerializer works awesome. See sample code below:

    public static string XmlSerialize(this object input)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            DataContractSerializer serializer = new DataContractSerializer(input.GetType());
            serializer.WriteObject(stream, input);
            return new UTF8Encoding().GetString(stream.ToArray());
        }
    }

    public static T XmlDeserialize<T>(this string input)
    {
        using (MemoryStream memoryStream = new MemoryStream(new UTF8Encoding().GetBytes(input)))
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(T));
            return (T)serializer.ReadObject(memoryStream);
        }
    }
2

one way to solve this problem is have your class implement the interface IXmlSerializable. Implementing this interface forces the serializer to call the 'overridden' WriteXml and ReadXml mathods.

something like that :

public void WriteXml(XmlWriter w)
{
    wr.WriteStartElement("playersConnected"); 
    w.WriteElementString("name", Name);
    w.WriteElementString("connected-on" , ConnectedOn.ToString("dd.MM.yyyy HH:mm:ss"));
    //etc...
}

and when you read it :

DateTimeOffset offset;

if(DateTimeoffset.TryParse(reader.Value, out offset))
{
    connectedOn = offset;
}

it is a hassle but I cannot thing of any other way. also this solution gives you full control on your serialization process(this is the upside)

if you like this solution and want the complete one please comment and i will write it down

regarding the namespaces - i don't think you can get rid of it(i won't get the bonus score probably).

Vitor Canova
  • 3,918
  • 5
  • 31
  • 55
Itai Zolberg
  • 108
  • 2
  • 7
2

I've ended up just doing this...

Added the two extension methods ...

public static double ToUnixEpoch(this DateTimeOffset value)
{
    // Create Timespan by subtracting the value provided from 
    //the Unix Epoch then return the total seconds (which is a UNIX timestamp)
    return (double)((value - new DateTime(1970, 1, 1, 0, 0, 0, 0)
        .ToLocalTime())).TotalSeconds;
}

public static string ToJsonString(this DateTimeOffset value)
{
    return string.Format("\\/Date({0})\\/", value.ToUnixEpoch());
}

Modified the ViewData class...

[XmlRoot("playersConnected")]
public class PlayersConnectedItem
{
    public string name { get; set; }
    public string connectedOn { get; set; }
    public string server { get; set; }
    public string gameType { get; set; }
}

Changed how I set the viewdata properties...

var data = (from q in connectedPlayerLogEntries
            select new PlayersConnectedItem
                       {
                           name = q.ClientName,
                           connectedOn =  q.CreatedOn.ToJsonString(),
                           server = q.GameFile.UniqueName,
                           gameType = q.GameFile.GameType.Description()
                        });

Done. Not sure if that's the best way .. but now that viewdata property has the same values for either Json or Xml.

Pure.Krome
  • 84,693
  • 113
  • 396
  • 647
-1

To supplement @Peter's answer, if you're using an ADO.NET Entities model (.edmx), and hence all the access modifiers are generated automatically in partial classes, you can edit the T4 template (expand the .edmx file in the solution explorer and open YourEdmxFilename.tt) to make it generate DateTimeOffset types with the internal modifier instead of the default.

Simply replace the Property(EdmProperty) method with the one below:

public string Property(EdmProperty edmProperty)
{
    string typeName = _typeMapper.GetTypeName(edmProperty.TypeUsage);
    return string.Format(
        CultureInfo.InvariantCulture,
        "{0} {1} {2} {{ {3}get; {4}set; }}",
        typeName == "System.DateTimeOffset" || typeName == "Nullable<System.DateTimeOffset>" ? "internal" : Accessibility.ForProperty(edmProperty),
        typeName,
        _code.Escape(edmProperty),
        _code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
        _code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
}
Extragorey
  • 1,654
  • 16
  • 30