关于c#:如何优雅地处理时区

How to elegantly deal with timezones

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

  • 获取当前时间(使用DateTime.UtcNow很容易解决)。

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

    这也会给使用像JsonResult这样的东西增加困难。你再也不能轻易地打电话给Json(myEnumerable),必须是Json(myEnumerable.Select(transformAllDates))。也许automapper能在这种情况下有所帮助?

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

  • 以下是我在视图中考虑使用的扩展:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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将要使用的时间)然后使用ModelBinder转换为UTC是否有意义?用户的时区存储在数据库中,并且很容易检索到。


    不是说这是一个建议,它更共享一个范例,而是我所看到的处理Web应用程序中时区信息(这不是ASP.NET MVC独有的)的最强烈方式是:

    • 服务器上的所有日期时间都是UTC。这意味着,就像你说的,使用DateTime.UtcNow

    • 尽量不要信任将日期传递给服务器的客户端。例如,如果您需要"现在",不要在客户机上创建日期,然后将其传递给服务器。要么在GeT中创建一个日期并将其传递给ViewModel,要么在Do-DateTime.UtcNow之后创建。

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

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

    • 在呈现视图时,他们使用html5 元素,永远不会直接在视图模型中呈现日期时间。它作为HtmlHelper扩展实现,类似于Html.Time(Model.when)。它将使变为。

      然后他们将使用javascript将UTC时间转换为客户机的本地时间。脚本将查找所有元素,并使用date-format数据属性来格式化日期并填充元素的内容。

    这样他们就不必跟踪、存储或管理客户的时区。服务器不关心客户机在哪个时区,也不需要进行任何时区转换。它只需吐出UTC,让客户将其转换为合理的内容。这在浏览器中很容易,因为它知道它在哪个时区。如果客户端更改了他/她的时区,Web应用程序将自动更新自己。它们存储的唯一内容是用户区域设置的日期时间格式字符串。

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


    在几次反馈之后,这里是我的最终解决方案,我认为这是干净和简单的,涵盖了日光节约问题。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        public class Quote
        {
            ...
            public DateTime DateCreated
            {
                get { return CRM.Global.ToLocalTime(_DateCreated); }
                set { _DateCreated = value.ToUniversalTime(); }
            }
            private DateTime _DateCreated { get; set; }
            ...
        }

    2-在全局帮助程序中,我们将自定义函数"toLocalTime":

    1
    2
    3
    4
    5
    6
    7
        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来进一步改进这一点,这样我们就可以从用户类中检索,而不是使用恒定的"中国标准时间":

    1
    2
    3
    4
    5
    6
    public class Contact
    {
        ...
        public string TimeZone { get; set; }
        ...
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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中,下面是最终结果:

    1
    2
    3
    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指出了原始解决方案的薄弱环节,并为删除原始文章感到抱歉,但在获取正确的代码显示格式时遇到了问题…原来编辑把"子弹"和"预编码"混在一起有问题,所以我去掉了"牛头",没问题。


    在对事件的sf4answers部分,用户输入地址的安全事件,以及一个安全的optional开始日期和结束日期。这些时代是一个datetimeoffset翻译成SQL Server,打印的帐户从GMT为粒状。

    这是你在面对相同的问题(虽然你是以一个不同的方法对它,你是在使用DateTime.UtcNow);你有一个你需要的位置和时间timezone翻译不能从一个到另一个。

    有两个主要的事情我做的,是为我工作。首先,使用datetimeoffset结构,总是。它从GMT为粒状和账户,如果你能把信息从你的client,它使你的生活容易一点。

    第二,当进行假设的翻译,你知道的,client位置/时间是在监狱中,你可以使用的公共数据库的信息时代翻译不能从大区到另一个时间GMT时间区(或triangulate之间,如果你会,两次zones)。关于the的东西tz数据库(有时被称为"大账户数据库Olson)是变化的,它的时间为zones的整个历史是一个功能的安全得到粒状的日期,你想得到粒状(只是看在2005年的能源政策法案的改变,当日光储蓄时间日期在不影响按成他在美国)。

    与该数据库在手,你可以使用的zoneinfo(tz数据库/数据库API .net Olson)。注意,不是二进制分布有一个,你会有大的最新版本下载和编译时它自己。

    在本文的写作时间,它parses目前所有的最新文件在数据分布(我跑它实际上是对的/ elsie.nci.nih.gov FTP:文件/酒吧/ tzdata2011k.tar.gz在九月25,2011;在2017年三月,你会得到它通过HTTPS iana.org:/ / /时间zones或从FTP fpt.iana.org tz:/ / / / / tzdata2017a.tar.gz)发布的。

    在sf4answers相比后得到的地址,它是geocoded longitude组合成一个latitude /,然后发送到一个第三方网站的服务,得到对应于一个timezone tz进入安全的数据库。从那里,开始和结束的时间是与适当的情况下为该datetimeoffset成粒状,然后stored GMT在数据库。

    作为为处理和与它在websites相比,这取决于你的观众和我们都试图显示。如果你会注意到,大多数的社会websites(和比较,和在显示部分sf4answers事件)事件在相对安全的时间,或者,如果是用absolute价值,这是通常GMT。

    然而,如果你的expects当地观众的时代,然后使用安全的方法,datetimeoffset随着时间的延伸,以将大大区将只是罚款;该SQL数据类型的.net datetimeoffsetdatetimeoffset会翻译不能,然后你可以得到普遍使用GetUniversalTime方法的时候。从那里,你将使用的方法在大类的ZoneInfoGMT将从当地时间(你会有一个小的工作做datetimeoffset让它变成一个简单的,但它是足够大的原因)。

    大的转型的原因在哪里?这是一个成本的付出,你是要有个地方,和有没有"最佳"的方式。我想opt的观点虽然为粒状,与作为部分的timezone观模型提出的观点。的方式,如果为观要求的改变,你不改变你的观点有很大的变化accommodate模型。你的JsonResult会包含一个简单的模型与IEnumerable和粒状。

    在一个模型的输入侧,采用binder?我想说,没有绝对的方法。你不能guarantee(现在所有的日期,或在未来将有大)就在这种方式,它应该是很明确的功能的控制器进行大的这一行动。再次,如果你不改变的要求,有一个或许多情况下tweak ModelBinderadjust大的业务逻辑和业务逻辑是,这意味着它应该是在控制器。


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


    对于输出,创建这样的显示/编辑器模板

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

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

    有关创建自定义编辑器模板的详细信息,请参阅此处和此处。

    或者,由于您希望它既适用于输入又适用于输出,我建议您扩展一个控件,甚至创建自己的控件。这样,您就可以截取输入和输出,并根据需要转换文本/值。

    如果你想沿着这条路走下去,这个链接很有希望把你推向正确的方向。

    不管怎样,如果你想要一个优雅的解决方案,它将是一个有点工作。好的一面是,一旦你完成了它,你就可以把它保存在代码库中以备将来使用!


    这可能是破解一个螺母的重锤,但您可以在UI和业务层之间插入一个层,该层透明地将返回的对象图上的日期时间转换为本地时间,并在输入日期时间参数上转换为UTC。

    我想这可以通过使用Postharp或一些反向控制容器来实现。

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