如何优雅地处理时区


140

我的网站所在的时区与使用该应用程序的用户所在的时区不同。除此之外,用户可以具有特定的时区。我想知道其他SO用户和应用程序如何实现这一目标?最明显的部分是在数据库内部,日期/时间存储在UTC中。在服务器上时,所有日期/时间都应以UTC处理。但是,我看到了我要克服的三个问题:

  1. 以UTC格式获取当前时间(通过轻松解决DateTime.UtcNow)。

  2. 从数据库中提取日期/时间并将其显示给用户。有潜在的很多人要求在不同的视图上打印日期。我在考虑视图和控制器之间的某些层可以解决此问题。或启用自定义扩展方法DateTime(请参见下文)。主要缺点是,在视图中使用日期时间的每个位置,都必须调用extension方法!

    这也会增加使用类似 JsonResult。您不能再轻易打电话了Json(myEnumerable),那一定是Json(myEnumerable.Select(transformAllDates))。也许AutoMapper可以在这种情况下提供帮助?

  3. 从用户处获取输入(本地到UTC)。例如,发布带有日期的表单将需要将日期转换为UTC之前的日期。首先想到的是创建一个自定义ModelBinder

这是我认为在视图中使用的扩展:

public static class DateTimeExtensions
{
    public static DateTime UtcToLocal(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
    }

    public static DateTime LocalToUtc(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
        return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
    }
}

考虑到很多应用程序现在都是基于云的,因此我认为考虑到时区是一件很平常的事,因为服务器的本地时间可能与预期的时区有很大差异。

之前是否已经解决过这个问题?有什么我想念的吗?想法和想法深表赞赏。

编辑:为了消除一些混乱,我想添加一些更多的细节。现在的问题不是如何在数据库中存储UTC时间,而是更多关于从UTC-> Local和Local-> UTC开始的过程。正如@Max Zerbini指出的那样,将UTC-> Local代码放在视图中显然是明智的,但是使用的是DateTimeExtensions真正的答案吗?从用户那里获取输入信息时,接受日期作为用户的本地时间(因为这就是JS会使用的时间),然后使用a ModelBinder转换为UTC 有意义吗?用户的时区存储在数据库中,很容易检索。


3
您可能首先阅读了这篇出色的文章。

@dodgy_coder-一直是时区的重要资源链接。但是,它并不能真正解决我的任何问题(特别是与MVC有关的问题)。不过谢谢
TheCloudlessSky's


好奇您选择了哪种解决方案。我自己也面临类似的决定。好问题。
肖恩

@Sean-到目前为止,解决方案还不是很完美(这就是为什么我还没有接受答案)。打印日期/时间并使用手动将其转换回原来的工作量很大ModelBinder
TheCloudlessSky

Answers:


106

并不是说这是一个建议,而是它更多地共享了一个范例,但是我所见过的在Web应用程序中处理时区信息的最积极的方式是:

  • 服务器上的所有日期时间均为UTC。如您所说,这意味着使用DateTime.UtcNow

  • 尽量少信任客户端将日期传递到服务器。例如,如果您需要“现在”,则不要在客户端上创建日期,然后将其传递给服务器。在GET中创建一个日期,然后将其传递给ViewModel或在POST上执行DateTime.UtcNow

到目前为止,这是相当标准的票价,但这是事情变得“有趣”的地方。

  • 如果您必须接受来自客户端的日期,请使用javascript确保要发布到服务器的数据采用UTC。客户知道它所在的时区,因此它可以合理的精度将时间转换为UTC。

  • 渲染视图时,他们使用的是HTML5 <time>元素,它们永远不会直接在ViewModel中渲染日期时间。它被实现为HtmlHelper扩展,类似于Html.Time(Model.when)。它会渲染<time datetime='[utctime]' data-date-format='[datetimeformat]'></time>

    然后,他们将使用javascript将UTC时间转换为客户端本地时间。该脚本将查找所有<time>元素,并使用date-formatdata属性设置日期格式并填充元素的内容。

这样,他们就不必跟踪,存储或管理客户的时区。服务器不在乎客户端所在的时区,也不必进行任何时区转换。它只是吐出UTC,让客户将其转换为合理的东西。在浏览器中这很容易,因为它知道它所在的时区。如果客户端更改了其时区,则Web应用程序将自动更新自身。他们存储的唯一内容是用户语言环境的日期时间格式字符串。

我并不是说这是最好的方法,但这是我从未见过的另一种方法。也许您会从中收集一些有趣的想法。


感谢您的答复。从用户那里获取日期输入(例如安排约会)时,是否不假定此输入位于其当前时区?您对ModelBinder在执行操作之前进行此转换的想法如何(请参阅我对@casperOne的评论。此外,这个<time>想法还不错。唯一的缺点是,这意味着我需要在整个DOM中查询这些元素,只是为了转换日期不是很好(对JS禁用了什么?)。再次感谢!
TheCloudlessSky 2011年

通常,是的,假设是它将位于本地时区。但是他们提出了一点,以确保他们每次从用户那里收集时间后,在将其发送到服务器之前,都使用javascript将其转换为UTC。他们的解决方案非常笨重,并且在关闭JS时并没有很好地降级。我还没有考虑到足够的聪明之处。但是就像我说的那样,它可能会给您一些想法。
J. Holmes

2
此后,我从事了另一个需要本地日期的项目,这就是我使用的方法。我正在使用moment.js进行日期/时间格式化……这很不错。谢谢!
TheCloudlessSky

是否没有一种简单的方法在应用程序的app.config / web.cofig中定义应用程序范围的时区,并使其将所有DateTime.Now值转换为DateTime.UtcNow?(我想避免公司的程序员仍然会错误地使用DateTime.Now的情况)
Uri Abramson

3
我看到的这种方法的一个缺点是,当您将时间用于其他事物,创建pdf,电子邮件等时,由于没有时间元素,因此您仍然必须手动转换它们。否则,非常整洁的解决方案
shenku 2014年

15

经过几次反馈,这是我的最终解决方案,我认为它是干净简单的,涵盖了夏时制问题。

1-我们在模型级别处理转换。因此,在Model类中,我们编写:

    public class Quote
    {
        ...
        public DateTime DateCreated
        {
            get { return CRM.Global.ToLocalTime(_DateCreated); }
            set { _DateCreated = value.ToUniversalTime(); }
        }
        private DateTime _DateCreated { get; set; }
        ...
    }

2-在全局帮助器中,我们使我们的自定义函数“ ToLocalTime”:

    public static DateTime ToLocalTime(DateTime utcDate)
    {
        var localTimeZoneId = "China Standard Time";
        var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
        return localTime;
    }

3-我们可以通过在每个用户配置文件中保存时区ID来进一步改善这一点,以便我们可以从用户类别中检索而不是使用常量“中国标准时间”:

public class Contact
{
    ...
    public string TimeZone { get; set; }
    ...
}

4-在这里,我们可以获取时区列表,以显示给用户以从下拉框中选择:

public class ListHelper
{
    public IEnumerable<SelectListItem> GetTimeZoneList()
    {
        var list = from tz in TimeZoneInfo.GetSystemTimeZones()
                   select new SelectListItem { Value = tz.Id, Text = tz.DisplayName };

        return list;
    }
}

因此,现在在中国上午9:25,网站托管在美国,日期保存在UTC的数据库中,这是最终结果:

5/9/2013 6:25:58 PM (Server - in USA) 
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)

编辑

感谢马特·约翰逊Matt Johnson)指出原始解决方案的薄弱环节,并为删除原始帖子感到抱歉,但是遇到了无法正确显示代码显示格式的问题……事实证明编辑器在将“项目符号”与“预代码”混合时遇到了问题,所以我删除了牛头,没关系。


如果某些人没有n层体系结构或共享Web库,这似乎是可行的。我们如何将时区ID共享到数据层。
维卡什·库马尔

9

sf4answers事件部分中,用户输入事件的地址以及开始日期和可选的结束日期。这些时间被翻译成datetimeoffset SQL Server中的,它解释了与UTC的偏移量。

这是您面临的相同问题(尽管您使用的是不同的方法,因为您正在使用 DateTime.UtcNow);您有一个位置,需要将时间从一个时区转换为另一个时区。

我做了两件对我有用的主要事情。一,使用DateTimeOffset结构,始终。它考虑了与UTC的抵销,并且如果您可以从客户那里获得该信息,那么您的生活会更加轻松。

其次,在执行翻译时,假设您知道客户端所在的位置/时区,则可以使用公共信息时区数据库将时间从UTC转换为另一个时区(如果需要,可以在两个之间进行三角测量)时区)。tz数据库(有时称为Olson数据库)的优点在于,它可以解释整个历史记录中时区的变化;获得补偿是您希望获得补偿的日期的函数(请看2005年的《能源政策法案》,法案更改了夏令时在美国生效的日期))。

有了数据库,您可以使用ZoneInfo(tz数据库/ Olson数据库).NET API。请注意,这里没有二进制发行版,您必须下载最新版本并自己编译。

在撰写本文时,它目前正在解析最新数据分发中的所有文件(我实际上在9月25日针对ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz文件运行了该文件, 2011年; 2017年3月,您可以通过https://iana.org/time-zonesftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz来获取它。

因此,在sf4answers上,获取地址后,会将其地理编码为纬度/经度组合,然后发送至第三方Web服务以获取与tz数据库中的条目相对应的时区。从那里开始时间和结束时间转换为DateTimeOffset具有适当UTC偏移量的实例,然后存储在数据库中。

至于在SO和网站上进行处理,则取决于受众和您要显示的内容。如果您注意到,大多数社交网站(和SO,以及sf4answers的事件部分)会以相对时间内,或者,如果使用绝对值,则通常是UTC。

但是,如果您的听众期望当地时间,那么使用DateTimeOffset带时区转换的扩展方法就可以了;SQL数据类型datetimeoffset将转换为.NET DateTimeOffset,然后您可以获取使用该GetUniversalTime方法的通用时间。从那里开始,您只需使用ZoneInfo类上的方法即可将UTC转换为本地时间(您需要做一些工作才能将其转换为本地时间)。DateTimeOffset,但是这样做很简单)。

在哪里做改造?这是您必须在某处支付的费用,并且没有“最佳”方法。我还是选择视图,时区偏移作为呈现给视图的视图模型的一部分。这样,如果视图的需求发生了变化,则无需更改视图模型即可适应该变化。您JsonResult只需包含一个带有的模型IEnumerable<T> 偏移量。

在输入端,使用模型绑定程序?我绝对不会说。您不能保证必须以这种方式转换所有日期(现在或将来的日期),它应该是控制器的显式功能才能执行此操作。同样,如果需求发生变化,则无需调整一个或多个ModelBinder实例来调整业务逻辑。这业务逻辑,这意味着它应该在控制器中。


感谢您的详细回复。我希望我不会觉得粗鲁,但这并不能真正解决我对ASP.NET MVC的任何担忧:用户输入呢(使用自定义模型绑定程序就足够了)?最重要的是,如何显示这些日期(在用户的本地时区)?我觉得扩展方法将为我的视图增加很多分量,这似乎是不必要的。
TheCloudlessSky's

1
@TheCloudlessSky:请参阅我编辑后的回复的最后两段。就个人而言,我认为进行转换的细节很小。主要问题是日期时间数据的实际转换和存储(顺便说一句,我在这里不能强调datetimeoffset在SQL Server和DateTimeOffset.NET中的使用,它们确实极大地简化了事情),我相信.NET无法充分处理完全出于上述原因。如果您在2003年输入了一个在NYC中的日期,然后希望将其转换为2011年在LA中的一个日期,则.NET在这种情况下将很难失败。
casperOne 2011年

不使用模型绑定器(或操作之前的任何层)的问题是,每个控制器在处理日期时变得很难测试(它们都依赖于日期转换)。这样可以将单元测试日期写入UTC。我的应用程序具有与个人资料相关联的用户(请考虑与他们的执业相关的医生/秘书)。该配置文件保存时区信息。因此,很容易获得当前用户的个人资料的时区并在绑定期间进行转换。您对此有其他说法吗?感谢您的输入!
TheCloudlessSky's

反对的理由是您的测试无法准确反映测试用例。您的输入将不会使用UTC,因此不应精心设计测试用例。它应该使用带有位置和所有位置的真实日期(尽管使用DateTimeOffsetIMO,将大大减轻这一点)。
casperOne 2011年

是的-但这意味着每个处理日期的测试都必须考虑这一点。处理日期时间的每个操作都将始终转换为UTC。对我来说,这是模型联编程序的主要候选者,然后测试模型联编程序
TheCloudlessSky's

5

这只是我的看法,我认为MVC应用程序应将良好的数据表示问题与数据模型管理分开。数据库可以在本地服务器时间存储数据,但是表示层必须使用本地用户时区来呈现日期时间。在我看来,这与不同国家/地区的I18N和数字格式存在相同的问题。在您的情况下,您的应用程序应检测到Culture用户的和时区,并更改显示不同文本,数字和日期表示的视图,但是存储的数据可以具有相同的格式。


感谢您的答复。是的,这就是我基本描述的内容,我只是想知道如何使用MVC优雅地实现这一点。该应用程序将用户的时区存储为他们创建帐户的一部分(他们选择他们的时区)。问题再次出在如何在每个视图中呈现日期而又不必(希望)用我创建的方法来填充它们DateTimeExtensions
TheCloudlessSky's

有许多方法可以做到这一点,一种是按照您的建议使用辅助方法,另一种也许更复杂但更优雅的方法是使用处理请求和响应并转换日期时间的过滤器。另一种方法是开发自定义Display属性,以注释视图模型的DateTime类型的字段。
Massimo Zerbini

这些就是我一直在寻找的东西。如果你看一下我的OP,我也使用AutoMapper做一些处理,提到要查看/ JSON的结果,但动作已被执行之后。
TheCloudlessSky's

1

为了输出,像这样创建一个显示/编辑器模板

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

如果只希望某些模型使用这些模板,则可以基于模型上的属性来绑定它们。

看到这里这里创建自定义编辑器模板的更多详细信息,。

另外,由于您希望它同时适用于输入和输出,因此建议您扩展控件甚至创建自己的控件。这样,您可以截取输入和输出,并根据需要转换文本/值。

这个连结如果您想走这条路,希望可以将您引向正确的方向。

无论哪种方式,如果您想要一个优雅的解决方案,那么它都会有些工作。从好的方面来说,一旦完成,就可以将其保存在代码库中以备将来使用!


我认为自定义显示/编辑器模板和活页夹是最优雅的解决方案,因为一旦实现,它将“正常工作”。任何开发人员都不需要知道任何特殊信息即可使日期正确使用DisplayForEditorFor并且每次都能使用。+1
Kevin Stricker

17
这不会渲染应用程序服务器的时间,而不是浏览器的时间吗?
亚历克斯(Alex)

0

这可能是一个破烂不堪的工具,但是您可以在UI层和Business层之间插入一个层,该层将日期时间透明地转换为返回的对象图上的本地时间,并转换为输入日期时间参数上的UTC。

我想这可以使用PostSharp或控制容器的某种反转来实现。

就个人而言,我只想在用户界面中显式转换您的日期时间...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.