我的网站托管在与使用该应用程序的用户不同的时区。除此之外,用户还可以拥有特定的时区。我想知道其他SO用户和应用程序如何处理这个问题?最明显的部分是在DB内部,日期/时间以UTC格式存储。在服务器上时,所有日期/时间都应以UTC格式处理。但是,我看到了我要克服的三个问题:
以UTC格式获取当前时间(使用DateTime.UtcNow
轻松解决)。
从数据库中提取日期/时间并将其显示给用户。可能会有很多来调用不同视图上的日期。我在考虑可以解决这个问题的视图和控制器之间的某个层。或者在DateTime
上使用自定义扩展方法(见下文)。主要的缺点是在在视图中使用日期时间的每个位置,必须调用扩展方法!
这也会增加使用JsonResult
等内容的难度。您无法再轻松拨打Json(myEnumerable)
,它必须是Json(myEnumerable.Select(transformAllDates))
。也许AutoMapper可以在这种情况下提供帮助吗?
从用户获取输入(本地到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->本地和本地 - > UTC的过程。正如@Max Zerbini所指出的那样,将UTC-> Local代码放在视图中显然很聪明,但是真正使用DateTimeExtensions
答案呢?当从用户那里获得输入时,接受日期作为用户的本地时间(因为JS将使用的是什么)是否有意义,然后使用ModelBinder
转换为UTC?用户的时区存储在数据库中,可以轻松检索。
答案 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 US的ZoneInfo (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.gz或GetUniversalTime
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()))
如果您只希望某些模型使用这些模板,则可以根据模型上的属性绑定它们。
有关创建自定义编辑器模板的详细信息,请参阅here和here。
或者,既然你希望它同时适用于输入和输出,我建议扩展一个控件甚至创建你自己的控件。这样,您可以截取输入和输出,并根据需要转换文本/值。
如果你想沿着这条道路前进,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。
我希望这可以帮助有类似需求的人。