XmlSerializer Quiz: The solution
I posted a little question here, just asking "Why?". For your reading convenience, I pull the code in here once more:
DateTime issued;
[XmlIgnore]
public DateTime IssuedUtc
{ get { return issued; } set { issued = value; } }
[XmlElement("issued")]
public DateTime IssuedLocalTime
{ get { return Issued.ToLocalTime(); } set { Issued = value.ToUniversalTime(); } }
So, indeed, why am I doing this? Well, when I decided to normalize all times handled by the backend engine of dasBlog into the UTC timezone, I really didn't think of the XML Serializer being a problem at first. It turned out to be one.
The reason why I wanted all times to be handled internally as UTC is quite simple: Too many time zones to deal with and I need to have a proper reference to do forward and backward time calculations. dasBlog deals with four time-zones:
- "Reader Time": The "<%userWhen%> macro emits a block of script that will cause the browser to emit the time of a post local to the reader's time zone. That one is easy, because the calculation happens on the client, but I need to feed it UTC (GMT).
- "Display Time": This is the time zone the blog owner selects for his/her blog. All times displayed on the weblog pages are shown in that time zone (complete with the TZ name and the GMT offset). This also applies to the "admin pages" such as referrals and events (which both roll over in synch with the display time zone). Display time is calculated dynamically and you will notice that it also automatically adjusts for daylight savings time. The display time zone is also by no means fixed. If the blog author travels (and you will see this on my blog starting next week), he/she can adjust the blog to his/her present time zone. When I am going to be at TechEd Malaysia, my blog will show UTC+0800. To make this time-zone shifting work, the absolute time must be stored in UTC.
- "Engine Time": This is UTC. All of the dasBlog runtime handles everything in UTC.
- "Server Time": Now were getting to the point. The engine runs on a server that has it's own local time zone setting: "server local time". That's something that the user who's running his engine at some ISP can't control and that's the one of all the time zones that really nobody is ever interested in. You shouldn't care whether your blog is hosted in Germany, the U.S. East Coast or Singapore. That's even more of an issue because one could expect that hosted sites may get moved around between ISP locations. The only little thing we're interested in is that the server knows its offset to UTC.
So ... I was thinking.... ask for DateTime.Now.ToUniversalTime(), handle everything in UTC from there on and everything's good. (Btw, I know about DateTime.UtcNow, but I like the expressiveness of this better).
What I wasn't considering is the way the XmlSerializer works, which I am using both for storage and for the various web services (including the Atom feed). What I also found is that the DateTime class in the framework isn't time-zone aware.
The workaround above is used because the XML Serialization infrastructure always assumes local time ("Server time") for serialization and will always emit ISO 8601 dates with a time-zone suffix like this: 2003-08-19T15:15:58.0781250+02:00. So when you are handling UTC times internally and use them blindly with XML serialization for both storage and web services, the serializer will assume you are using local time and throw you off by your own time-zone difference to UTC.
This isn't "too bad" when you store stuff in local XML files, because when you write something wrong from the same place you read it back into and your time-zones don't change you are in ok in memory, but you are nevertheless wrong on disk. What happened to me in dasBlog version 1.1 was that I was thinking that I stored UTC, but in fact I stored everything in local time. My UTC time 2003-08-19T15:15:58 always turned into 2003-08-19T15:15:58+02:00, because the DateTime class doesn't keep time-zone information around that the serializer could use. Therefore the serializer must always assume local time and that causes the offset to be emitted. That's of course much worse for UTC+12.
The fix:
The field DateTime issued; holds the "engine time", which is always UTC.
The property
[XmlIgnore]
public DateTime IssuedUtc
{ get { return issued; } set { issued = value; } }
wraps this value and is the property that the engine works with internally. The XmlSerializer is instructed to ignore this value in the serialization process by declaration of the [XmlIgnore] attribute. Instead, we tell the serializer to look at the following property, which is not used by the engine itself and also indicates that by its name suffix "LocalTime", which essentially declares it as "off limits" for direct access to everyone knowing the project:
[XmlElement("issued")]
public DateTime IssuedLocalTime
{ get { return Issued.ToLocalTime(); } set { Issued = value.ToUniversalTime(); } }
This property is the one that the serializer grabs and it does the proper conversion to and from server local time that the serializer requires. The actual dasBlog code is using a variant of this property that is a bit larger in code size because it also checks for DateTime.MinValue and DateTime.MaxValue occurrences, which, depending on the server time zone, would cause the time zone shifting to fail with an overflow/underflow exception (and no, I am not checking near MinValue and MaxValue):
[XmlElement("Date")]
{
get
{
return (DateUtc==DateTime.MinValue||DateUtc==DateTime.MaxValue)?
DateUtc:DateUtc.ToLocalTime();
}
set
{
DateUtc = (value==DateTime.MinValue||value==DateTime.MaxValue)?
value:value.Date.ToUniversalTime();
}
}
So, that's why.
Be aware that all of this applies to ASP.NET Web Services, too, and if you are dealing with multiple time zones are you are using UTC normalized times in your app, you will have to deal with this. If "server time" makes you happy, you won't need to worry.