如何优雅地处理时区

时间:2011-09-28 01:28:54

标签: c# .net asp.net-mvc asp.net-mvc-3 timezone

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

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

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

    这也会增加使用JsonResult等内容的难度。您无法再轻松拨打Json(myEnumerable),它必须是Json(myEnumerable.Select(transformAllDates))。也许AutoMapper可以在这种情况下提供帮助吗?

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

  4. 以下是我想在视图中使用的扩展程序:

    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->本地和本地 - > UTC的过程。正如@Max Zerbini所指出的那样,将UTC-> Local代码放在视图中显然很聪明,但是真正使用DateTimeExtensions答案呢?当从用户那里获得输入时,接受日期作为用户的本地时间(因为JS将使用的是什么)是否有意义,然后使用ModelBinder转换为UTC?用户的时区存储在数据库中,可以轻松检索。

7 个答案:

答案 0 :(得分:100)

这不是一个推荐,它更多地分享了一个范例,但是我看到了在Web应用程序中处理时区信息的最常见的 agressive 方式(这不是ASP.NET独有的) MVC)如下:

  • 服务器上的所有日期时间均为UTC。 这意味着像你说的那样使用DateTime.UtcNow

  • 尽量不要相信客户端尽可能少地将日期传递给服务器。例如,如果您需要“now”,请不要在客户端上创建日期,然后将其传递给服务器。在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-format数据属性格式化日期并填充元素的内容。

通过这种方式,他们无需跟踪,存储或管理客户端时区。服务器不关心客户端的时区,也不关心任何时区翻译。它只是吐出UTC并让客户端将其转换为合理的东西。这很容易从浏览器中获取,因为它知道它所在的时区。如果客户端更改了他/她的时区,Web应用程序将自动更新。他们存储的唯一内容是用户区域设置的日期时间格式字符串。

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

答案 1 :(得分:13)

经过多次反馈,这是我的最终解决方案,我认为它简洁明了,涵盖了夏令时问题。

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指出原始解决方案的薄弱环节,并且抱歉删除原始帖子,但是问题得到了正确的代码显示格式...结果编辑器在将“子弹”与“预编码“,所以我删除了bulles,这没关系。

答案 2 :(得分:8)

events section sf4answers上,用户输入活动的地址,以及开始日期和可选的结束日期。这些时间被转换为SQL服务器中的datetimeoffset,用于说明与UTC的偏移量。

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

我做了两件主要的事情对我有用。首先,始终使用DateTimeOffset structure。它占据了与UTC的偏差,如果您可以从客户那里获得这些信息,它会让您的生活更轻松。

其次,在执行翻译时,假设您知道客户端所在的位置/时区,您可以使用public info time zone database将时间从UTC转换为另一个时区(或三角测量,如果您愿意的话) ,在两个时区之间)。关于tz数据库(有时称为Olson database)的好处是它可以解释整个历史中时区的变化;获得偏移是您想要获得偏移的日期的函数(只需查看Energy Policy Act of 2005 changed the dates when daylight savings time goes into effect in the USZoneInfo (tz database / Olson database) .NET API

掌握数据库后,您可以使用latest version。请注意,没有二进制发行版,您必须下载ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz并自行编译。

在撰写本文时,它目前正在解析最新数据分发中的所有文件(我实际上是在2011年9月25日对https://iana.org/time-zones文件进行了分析; 2017年3月,你会得到它通过ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gzGetUniversalTime method)。

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

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

但是,如果您的受众需要本地时间,那么使用DateTimeOffset以及将时区转换为的扩展方法就可以了。 SQL数据类型datetimeoffset将转换为.NET DateTimeOffset,然后您可以获得使用JsonResult的通用时间。从那里,您只需使用ZoneInfo类上的方法将UTC转换为本地时间(您需要做一些工作才能将其转换为DateTimeOffset,但这很简单)。

在哪里进行转型?这是你需要花费某处的成本,并且没有“最佳”方式。我会选择视图,时区偏移作为视图模型的一部分呈现给视图。这样,如果视图的要求发生变化,则无需更改视图模型以适应更改。您的IEnumerable<T>只会包含ModelBinder 偏移量的模型。

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

答案 3 :(得分:5)

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

答案 4 :(得分:2)

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

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

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

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

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

如果你想沿着这条道路前进,

This link希望能帮助你朝着正确的方向前进。

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

答案 5 :(得分:0)

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

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

就个人而言,我只是明确地在UI中转换您的日期时间......

答案 6 :(得分:0)

我想将日期存储为DateTimeOffset,以便可以维护写入数据库的用户的时区偏移量。但是,我只想在应用程序内部使用DateTime。

因此,输入本地时区,输出本地时区。无论用户在何处,何时何地查看数据,对于观察者来说都是本地时间-更改存储为UTC +本地偏移量。

这是我实现这一目标的方式。

1。 首先,我需要获取Web客户端的本地时区偏移并将此值存储在Web服务器上:

// Sets a session variable for local time offset from UTC
function SetTimeZone() {
    var now = new Date();
    var offset = now.getTimezoneOffset() / 60;
    var sign = offset > 0 ? "-" : "+";
    var offset = "0" + offset;
    offset = sign + offset + ":00";
    $.ajax({
        type: "post",
        url: prefixWithSitePathRoot("/Home/SetTimeZone"),
        data: { OffSet: offset },
        datatype: "json",
        traditional: true,
        success: function (data) {
            var data = data;
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert("SetTimeZone failed");
        }
    });
}

该格式旨在与SQL Server DateTimeOffset类型的格式匹配。

SetTimeZone-仅设置Session变量的值。当用户登录时,我将此值合并到用户配置文件缓存中。

2。 当用户向数据库提交更改时,我通过实用程序类过滤DateTime值:

cmdADO.Parameters.AddWithValue("@AwardDate", (object)Utility.ConvertLocal2UTC(theContract.AwardDate, theContract.TimeOffset) ?? DBNull.Value);

方法:

public static DateTimeOffset? ConvertLocal2UTC(DateTime? theDateTime, string TimeZoneOffset)
{
    DateTimeOffset? DtOffset = null;
    if (null != theDateTime)
    {
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        DateTime datetimeUTC = datetime.ToUniversalTime();

        DtOffset = new DateTimeOffset(datetimeUTC.Ticks, AmountOfTime);
    }
    return DtOffset;
}

3。 当我从SQL Server中读取日期时,我正在这样做:

theContract.AwardDate = theRow.IsNull("AwardDate") ? new Nullable<DateTime>() : DateTimeOffset.Parse(Convert.ToString(theRow["AwardDate"])).DateTime;

在控制器中,我修改了日期时间以匹配观察者的本地时间。 (我确信有人可以通过扩展或其他方式做得更好):

theContract.AwardDate = Utilities.ConvertUTC2Local(theContract.AwardDate, CachedCurrentUser.TimeZoneOffset);

方法:

public static DateTime? ConvertUTC2Local(DateTime? theDateTime, string TimeZoneOffset)
{
    if (null != theDateTime)
    {
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        datetime = datetime.Add(AmountOfTime);
        theDateTime = new DateTime(datetime.Ticks, DateTimeKind.Utc);
    }
    return theDateTime;
}

在视图中,我只是显示/编辑/验证DateTime。

我希望这可以帮助有类似需求的人。