如何优雅地处理时区

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

  1. 以UTC获取当前时间(使用DateTime.UtcNow轻松解决)。

  2. 从数据库中提取date/时间并将其显示给用户。 在不同的视图上可能有很多打印date的电话。 我正在考虑可以解决这个问题的视图和控制器之间的一层。 或者在DateTime上有一个自定义的扩展方法(见下文)。 主要的缺点是,在视图中使用date时间的每个位置都必须调用扩展方法!

    这也会增加使用类似JsonResult 。 你不能再轻易调用Json(myEnumerable) ,它必须是Json(myEnumerable.Select(transformAllDates)) 。 也许AutoMapper可以帮助在这种情况下?

  3. 从用户获取input(本地到UTC)。 例如,使用date发布表单需要将date转换为UTC。 首先想到的是创build一个自定义的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真的是答案吗? 当从用户那里获得input时,接受date作为用户的本地时间是否合理(因为JS会使用这个date),然后使用ModelBinder转换为UTC? 用户的时区存储在数据库中,很容易检索。

这并不是说这是一个build议,它更多地分享了一个范例,但是我在处理Web应用程序中的时区信息(ASP.NET MVC不是排他性的)中看到的最激进的方式如下:

  • 服务器上的所有date时间均为UTC。 这意味着使用,就像你说的, DateTime.UtcNow

  • 尽量不要相信客户端将date传递给服务器。 例如,如果您需要“现在”,则不要在客户端上创builddate,然后将其传递给服务器。 在您的GET中创builddate并将其传递给ViewModel或POST DateTime.UtcNow

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

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

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

    然后,他们会使用javascript将UTC时间翻译成客户当地时间。 该脚本将查找所有<time>元素,并使用date-format数据属性来格式化date并填充元素的内容。

这样他们就不必跟踪,存储或pipe理客户时区。 服务器不关心客户端在什么时区,也不必做任何时区翻译。 它只是吐出UTC,让客户把它转换成合理的东西。 浏览器很容易,因为它知道它在什么时区。如果客户端改变他/她的时区,Web应用程序会自动更新自己。 他们唯一存储的是用户的语言环境的date时间格式string。

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

经过几次反馈,这是我认为是最简单的解决scheme,包括夏令时问题。

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 – 我们可以进一步改进,通过在每个用户configuration文件中保存时区id,以便我们可以从用户类中检索,而不是使用常量“中国标准时间”:

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

4 – 在这里,我们可以得到时区列表,以显示给用户从下拉框中select:

 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分,网站在美国托pipe,date在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) 

编辑

感谢马特·约翰逊指出了原来的解决scheme的弱点,并遗憾地删除原来的post,但得到的问题得到正确的代码显示格式…原来编辑有混合“子弹”与“前代码”的问题,所以我删除了凸起,这是确定的。

在sf4answers的活动部分 ,用户input一个事件的地址,以及一个开始date和一个可选的结束date。 这些时间被转换为SQL服务器中的datetimeoffset ,它占UTC的偏移量。

这是你面对的同样的问题(虽然你正在采取不同的方法,因为你使用的是DateTime.UtcNow ); 你有一个位置,你需要翻译一个时间从一个时区到另一个。

我做了两件主要的事情为我工作。 首先,总是使用DateTimeOffset结构 。 它占UTC的偏移量,如果你能从你的客户那里得到这些信息,它会让你的生活变得更容易一些。

其次,在执行翻译时,假设您知道客户所在的位置/时区,则可以使用公共信息时区数据库将UTC从另一个时区转换为另一个时区(如果您愿意,时区)。 关于tz数据库(有时被称为Olson数据库 )的好处在于它说明了整个历史时区的变化; 获得抵消是您想要抵消的date的函数(只要看看2005年 “ 能源政策法案”(Energy Policy Act),它改变了夏令时在美国生效的date )。

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

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

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

至于在SO和网站上处理它,这取决于观众和你想要展示什么。 如果你注意到,大多数社交网站(和SO以及sf4answers上的事件部分)在相对时间显示事件,或者如果使用绝对值,通常是UTC。

但是,如果您的观众期望当地时间,那么使用DateTimeOffset以及将时区转换为的扩展方法就可以了; SQL数据typesdatetimeoffset将转换为.NET DateTimeOffset ,然后您可以获取使用GetUniversalTime方法的通用时间。 从那里开始,只需使用ZoneInfo类中的方法将UTC从本地时间转换为本地时间(您将不得不做一些工作来将其转换为DateTimeOffset ,但这很简单)。

在哪里做转型? 这是你将不得不在某个地方支付的成本,而且没有“最好的”方法。 尽pipe我select了视图,但将时区偏移作为视图模型的一部分呈现给视图。 这样,如果对视图的要求发生变化,则不必更改视图模型以适应更改。 你的JsonResult只包含一个带IEnumerable<T>的模型偏移量。

在input端,使用模型绑定器? 我会说绝对没有办法。 您不能保证所有date(现在或将来)都必须以这种方式进行转换,它应该是您的控制器执行此操作的明确function。 同样,如果需求发生变化,您不必调整一个或多个ModelBinder实例来调整业务逻辑; 这业务逻辑,这意味着它应该在控制器中。

这只是我的看法,我认为MVC应用程序应该把数据表示问题和数据模型pipe理分开。 数据库可以将数据存储在本地服务器时间,但表示层使用本地用户时区呈现date时间是一种职责。 这在我看来与I18N和不同国家的数字格式相同的问题。 在你的情况下,你的应用程序应该检测用户的Culture和时区,并改变视图显示不同的文本,数字和datime的演示文稿,但存储的数据可以有相同的格式。

对于输出,创build一个像这样的显示/编辑器模板

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

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

有关创build自定义编辑器模板的更多详情,请参阅此处和此处

另外,既然你希望它能同时用于input和输出,我会build议扩展一个控件甚至创build你自己的。 这样,你可以拦截input和输出,并根据需要转换文本/值。

如果你想走下去, 这个链接将有希望把你推向正确的方向。

无论哪种方式,如果你想要一个优雅的解决scheme,它将是一个工作。 好的一面,一旦你完成了,你可以保存在你的代码库中,以备将来使用!

这可能是破解坚果的大锤,但是您可以在UI和业务层之间注入一层,在返回的对象图上将date时间透明地转换为本地时间,并在inputdate时间参数上显示UTC。

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

就我个人而言,我只是明确地转换你的date在UI中…