.NET 往返服务器时的时区和夏令时

Timezones and Daylight saving in .NET round trip to server

我们的应用程序在特定时区的特定日期遇到问题,在从服务器到客户端然后从客户端到服务器的往返过程中,DateTime 的值未保留。这是在巴西利亚时区(“东美洲标准时间”)中观察到的,日期时间值为“1984-11-04 00:00:00”。

我能够使用以下代码重现此问题:

DateTime d = new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local);
var dUtc = d.ToUniversalTime();
var dRtLocal = dUtc.ToLocalTime();

dUTC 的最终值为“1984-11-04 03:00:00”(正确),dRtLocal 为“1984-11-04 01:00:00”(不太正确)。

我发现虽然巴西的夏令时从 1985 年才开始 Windows 对从 0001-01-01 到 2006-12-31 的日期有相同的规则并且根据这个规则夏令时将从这个确切的日期开始 (1984-11-04 00:00:00) 将时钟向前移动 1 小时。

除了这个时区的 DST 规则错误之外,我还发现了一些其他奇怪的行为和 TimeZone 和 TimeZoneInfo 方法的不一致结果 类(GetUtcOffset、IsAmbiguousTime、IsInvalidTime)。

举个例子(我电脑的当前时区设置为“E. South America Standard Time”):

    TimeZone.CurrentTimeZone.GetUtcOffset(new DateTime(1984,11,03,23,00,00, DateTimeKind.Local))
    returns -02:00

    TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").GetUtcOffset(new DateTime(1984,11,03,23,00,00, DateTimeKind.Local)) 
    returns -03:00

在第一种情况下,它似乎正在使用当年的 DST 规则并将其应用于 1984 年(2015 年夏令时将从 2015-10-18 开始)。第二个似乎在 Windows 中为这个时区应用 DST 规则。

除了以 UTC 格式使用和存储所有日期之外,还有什么解决方法可以避免这些问题? .NET 将 DST 规则应用于过去日期的方式是否真的存在错误,其中 DST 规则与当年的 DST 规则不同?

Update 在@matt-johnson 回答后,我做了更多测试并发现更多与无效 DateTime 相关的不一致行为。 正如马特指出的那样,有问题的日期是无效日期(根据 windows 规则)。但是如果 运行:

var isInvalid = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))

结果是错误的,尽管 windows DST 规则应该被认为是无效的。但是如果 运行:

var isInvalid2 = TimeZoneInfo.Local.IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))

现在结果是真的。请注意,我当前的时区是“E.南美洲标准时间”(TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time")。StandardName == TimeZoneInfo.Local.StandardName 为真)。

尝试使用 TimeZoneInfo.ConvertTimeToUtc 将 DateTime 转换为 UTC 会引发异常,正如 Matt

所指出的

您在 TimeZone class 中发现的行为(使用 current 规则,而不是正确的适用规则)有据可查 on MSDN:

The TimeZone class supports only a single daylight saving time adjustment rule for the local time zone. As a result, the TimeZone class can accurately report daylight saving time information or convert between UTC and local time only for the period in which the latest adjustment rule is in effect. In contrast, the TimeZoneInfo class supports multiple adjustment rules, which makes it possible to work with historic time zone data.

您应该考虑弃用 TimeZone class,并且只使用 TimeZoneInfo class.

关于转换不匹配,这个错误实际上是在DateTime上调用ToUniversalTime时导致的。您在 d 中提供的值恰好在 spring-forward 转换的时刻(就 Windows 而言)。这意味着从 00:00:0000:59:59.9999999 的值在那个日期是 无效的 。一天从上午 1:00 开始,而不是午夜。

考虑一下,您可能已经编写了以下代码,而不是调用 ToUniversalTime

var dUtc = TimeZoneInfo.ConvertTimeToUtc(d, TimeZoneInfo.Local);

您可能认为这是等效的,但此代码会引发异常,因为 d 中提供的输入已被 DST 转换跳过。 不会 出现在 DateTime.ToUniversalTime 中,因为传递了一个名为 TimeZoneInfoOptions.NoThrowOnInvalidTime 的内部标志,您可以看到 in the reference sources。同样有趣的是 NoThrowOnInvalidTime 的行为在 .NET 3.5 和 .NET 4.0 之间发生了变化。在您的示例中,.NET 3.5 下为 return 02:00 UTC,.NET 4.x 下为 03:00 UTC。我不确定我是否同意此更改,但这是往返不匹配的根本原因。

最后 - 如您所述,巴西 1984 年的时区与 Windows 包含的最早 2006 年时区数据不同。一般来说,Windows 时区不是历史信息的良好来源。相反,您应该考虑使用 TZDB time zones, which has history to at least 1970, and earlier in many cases. In .NET, you can do this with the Noda Time 库。等效区域为 "America/Sao_Paulo".

但是,仍然意识到即使使用 Noda Time,您也无法往返无效的本地 date/time。如果它在本地时区无效,则从 utc 到本地的转换永远不会产生该结果。