ASP.NET 核心 API 当后缀为 'Z' 时,ISO8601 未在本地时间解析

ASP.NET Core API ISO8601 not parsed in local time when suffix is 'Z'

场景: 我有一个 REST-API 需要一个 POST-Request。作为主体数据,传递了一个 ISO8601 格式的日期时间。

{
    "validate": "2019-12-02T23:00:00Z",
    "configuration": {},
    "sortId": 1
}

使用 MVC 中的模型绑定,日期时间会自动解析。该变量应该在 api-server 的本地时区。在这种情况下 Europe/Berlin。我希望时间(参考示例)在 2019-12-03:00:00:00。但这种情况并非如此。还差一小时

但是当我 post 以下内容时:

{
    "validate": "2019-12-02T23:00:00+00:00",
    "configuration": {},
    "sortId": 1
}

本地时区的解析按预期进行。因为客户端 posting 数据是用 JS 编写的,并使用默认的 Date.toISOString() 函数,所以我总是在结尾得到一个 'Z'。根据 ISO8601,这完全没问题。

Z 明确表示 UTC。 +00:00 没有。英国现在是 00:00,但夏天是 01:00。 1970 年全年使用夏令时(01:00)。

这里涉及到几个概念和一些历史。首先,DateTime 没有偏移量或时区的概念。一个DateTime只能是UTCLocalUndefined,根据其Kind属性.

使用DateTime表示偏移信息丢失。结果值需要转换为 something。为此,使用了机器的偏移量。也就是说,Web 服务计算机的 偏移量,而不是数据库服务器的偏移量。

然后,我们的容器或应用程序故障转移到具有默认 UTC 时区而不是我们配置的时区的机器。周末。

值得一读Falsehoods programmers believe about time,尤其是8. The machine that a program runs on will always be in the GMT time zone.

更好的解决方案是使用 DateTimeOffset,尽管即使这样也无法处理 DST 规则更改。

更好的解决方案是使用 IANA 时区名称并传递 Europe/Berlin 而不是偏移量。但这不是常见的用法。航空公司至少 post 飞行时间,包括偏移量和时区名称。

日期时间解析规则

DateTime 解析规则将 Z 或偏移量转换为 Local 并进行转换,将其他任何内容转换为 Unspecified 而不进行转换。这听起来很奇怪,但考虑到 DateTime 是为在桌面应用程序上工作而构建的,其中 Local 时间是有意义的。

这个代码

var values=new[]{
"2019-12-02T23:00:00+00:00",
"2019-12-02T23:00:00Z",
"2019-12-02T23:00:00"
};
foreach(var value in values)
{
    var dt=DateTime.Parse(value);
    Console.WriteLine($"{dt:s}\t{dt.Kind}");
}

产生:

2019-12-03T01:00:00  Local
2019-12-03T01:00:00  Local
2019-12-02T23:00:00  Unspecified

UTC 类型在这里消失了,作为航班预订系统的开发者,我一点也不喜欢 。飞行时间是机场当地时间,不是我的服务器。现在我必须将该值转换回 UTC 或其他格式,然后再将其保存到数据库中。我必须将其转换回原始 airport 偏移量以打印机票并发送通知。

如果有任何错误,即使是由于航空公司的错误,我也必须赔偿你们。

JSON.NET(真的API)解析规则

另一方面,

JSON.NET 将带有 Z 的字符串解析为 UTC,偏移量为 Local 并进行转换,没有偏移量为 Undefined。对于从任何地方接收请求的 API,UTC 更有用。大多数托管商和云服务默认也提供 UTC 机器。

此代码:

class It
{
    public DateTime Dt{get;set;}
}


var values=new[]{
"{'dt':'2019-12-02T23:00:00+00:00'}",
"{'dt':'2019-12-02T23:00:00Z'}",
"{'dt':'2019-12-02T23:00:00'}"
};
foreach(var value in values)
{
    var dt=JsonConvert.DeserializeObject<It>(value).Dt;
    Console.WriteLine($"{dt:s}\t{dt.Kind}");
}

产生:

2019-12-03T01:00:00  Local
2019-12-02T23:00:00  Utc
2019-12-02T23:00:00  Unspecified

更好,但是不喜欢。我仍然丢失信息。

JSON 与 DateTimeOffset

DateTimeOffset 包含偏移量,因此不会丢失任何信息。未指定的偏移量被视为 Local 时间。这个片段:

class It
{
    public DateTimeOffset Dt{get;set;}
}
void Main()
{
    var values=new[]{
    "{'dt':'2019-12-02T23:00:00+00:00'}",
    "{'dt':'2019-12-02T23:00:00Z'}",
    "{'dt':'2019-12-02T23:00:00'}"
    };
    foreach(var value in values)
    {
        var dt=JsonConvert.DeserializeObject<It>(value).Dt;
        Console.WriteLine($"{dt:o}");
    }
    
}

产生:

2019-12-02T23:00:00.0000000+00:00
2019-12-02T23:00:00.0000000+00:00
2019-12-02T23:00:00.0000000+02:00

为什么不是到处都是 UTC?

因为这会丢失大量信息,很容易使该时间值无法使用。有 很多 的 SO 问题,其中 poster 试图比较 UTC 时间并由于 DST 影响甚至 DST 更改而得到意想不到的结果。

几年前,埃及在提前几周通知的情况下更改了夏令时规则。航空公司和在线代理商并不高兴。

此外,如果有两个以上的时区在玩怎么办?国际航班很少在同一时区降落,因此存储 UTC 不起作用。 航空公司不以 UTC 发布时刻表,他们以当地时间发布,并带有偏移量和 IANA TZ 名称作为附加信息。

值得一读Falsehoods programmers believe about time,尤其是涉及 UTC 或 GMT 的部分。