我们的应用程序是一个很大的n层ASP.NET MVC应用程序,它严重依赖于Dates和(local)Times。到目前为止,我们已经将DateTime
用于我们所有的模型,这些模型运行良好,因为多年来我们严格地是一个国家网站,处理单个时区。
现在情况发生了变化,我们正在为国际观众敞开大门。第一个想法是“哦,废话。我们需要重构整个解决方案!”
我们打开了LinQPad并开始草绘各种转换器,根据基于创建的DateTime
对象将常规DateTimeOffset
对象转换为TimeZoneInfo
个对象来自所述用户个人资料的用户的TimeZone ID值。
我们认为我们会将模型中的所有DateTime
属性更改为DateTimeOffset
并完成它。毕竟,我们现在拥有了存储和显示用户本地日期和时间所需的所有信息。
许多代码段都受Rick Strahl's blog post关于此主题的启发。
但后来我读了Matt Johnson's excellent comment。他验证了我打算切换到DateTimeOffset
声称:“DateTimeOffset在Web应用程序中必不可少”。
关于Noda Time,Matt说:
说到Noda Time,我不同意你的意见,你必须更换整个系统中的所有内容。当然,如果你这样做,你将有更少的机会犯错误,但你肯定可以在有意义的地方使用Noda Time。我个人致力于使用IANA时区进行时区转换的系统(例如“America / Los_Angeles”),但跟踪DateTime和DateTimeOffset类型中的所有其他内容。实际上很常见的是Noda Time在应用程序逻辑中广泛使用,但完全脱离了DTO和持久层。在某些技术中,如实体框架,如果你愿意,你不能直接使用Noda Time - 因为没有地方可以连接它。
这可以直接针对我们,因为我们现在正处于这种情况,包括我们选择使用IANA时区。
我们的主要目标是创建最不复杂的工作流程,以处理不同时区的日期和时间。在我们的服务,存储库和控制器中尽可能避免时区计算。
简而言之,该计划是接受我们前端的本地日期和时间,尽快将其转换为ZonedDateTime
并尽可能晚地将其转换为DateTimeOffset
将信息保存到数据库中。
确定正确ZonedDateTime
的关键因素是用户模型中的TimeZoneId
属性。
public class ApplicationUser : IdentityUser
{
[Required]
public string TimezoneId { get; set; }
}
为了防止大量重复代码,我们的计划是创建自定义ModelBinder,将本地DateTime
转换为ZonedDateTime
。
public class LocalDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
// Get the posted local datetime
string dt = request.Form.Get("DateTime");
DateTime dateTime = DateTime.Parse(dt);
// Get the logged in User
IPrincipal p = controllerContext.HttpContext.User;
var user = p.ApplicationUser();
// Convert to ZonedDateTime
LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezone = timeZoneProvider[user.TimezoneId];
var zonedDbDateTime = usersTimezone.AtLeniently(localDateTime);
return zonedDbDateTime;
}
}
我们可以使用这些模型粘合剂来丢弃控制器。
[HttpPost]
[Authorize]
public ActionResult SimpleDateTime([ModelBinder(typeof (LocalDateTimeModelBinder))] ZonedDateTime dateTime)
{
// Do stuff with the ZonedDateTime object
}
我们是否过度思考?
我们将使用concept of Buddy properties。说实话,我不是一个忠实的粉丝,因为它造成了混乱。新开发人员可能会因为我们有多种方法来保存创建日期这一事实。
非常欢迎有关如何改进这一建议的建议。我已阅读有关将属性从IntelliSense隐藏到将实际属性设置为private
的注释。
public class Item
{
public int Id { get; set; }
public string Title { get; set; }
// The "real" property
public DateTimeOffset DateCreated { get; private set; }
// Buddy property
[NotMapped]
public ZonedDateTime CreatedAt
{
get
{
// DateTimeOffset to NodaTime, based on User's TZ
return ToZonedDateTime(DateCreated);
}
// NodaTime to DateTimeOffset
set { DateCreated = value.ToDateTimeOffset(); }
}
public string OwnerId { get; set; }
[ForeignKey("OwnerId")]
public virtual ApplicationUser Owner { get; set; }
// Helper method
public ZonedDateTime ToZonedDateTime(DateTimeOffset dateTime, string tz = null)
{
if (string.IsNullOrEmpty(tz))
{
tz = Owner.TimezoneId;
}
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezoneId = tz;
var usersTimezone = timeZoneProvider[usersTimezoneId];
var zonedDate = ZonedDateTime.FromDateTimeOffset(dateTime);
return zonedDate.ToInstant().InZone(usersTimezone);
}
}
我们现在有一个基于Noda Time的应用程序。 ZonedDateTime对象使得进行临时计算和时区驱动查询变得更加容易。
这是正确的假设吗?
答案 0 :(得分:12)
首先,我必须说我印象深刻!这是一篇写得很好的文章,您似乎已经探讨了围绕这一主题的许多问题。
你的方法很好。但是,我将提供以下内容供您考虑改进。
模型绑定器可以改进。
我将其命名为ZonedDateTimeModelBinder
,因为您正在应用它来创建ZonedDateTime
值。
您希望使用bindingContext
获取值,而不是期望输入始终位于request.Form.Get("DateTime")
。您可以在the WebAPI model binder I wrote for LocalDate
中查看此示例。 MVC模型绑定器类似。
您还将在该示例中看到我如何使用Noda Time的解析功能而不是DateTime.Parse
。您可以考虑使用LocalDateTimePattern
。
确保您了解AtLeniently
的工作原理,以及我们为即将发布的2.0版本更改了其行为(有充分理由)。请参阅" Lenient解析器更改"在the migration guide的底部。如果这在您的域中很重要,您可能需要考虑通过实施自己的解析器来使用新行为。
您可能会认为可能存在当前用户的时区不是您当前正在使用的数据的时区的上下文。也许管理员正在使用其他一些用户的数据。因此,您可能需要将时区ID作为参数的重载。
对于常见情况,您可以尝试全局注册模型绑定器,这样可以节省控制器上的一些按键:
ModelBinders.Binders.Add(typeof(ZonedDateTime), new ZonedDateTimeModelBinder());
如果要传递参数,您仍然可以使用归因方式。
在代码的底部,ZonedDateTime.FromDateTimeOffset(dto).ToInstant().InZone(tz)
很好,但可以使用更少的代码完成。这些都是等价的:
ZonedDateTime.FromDateTimeOffset(dto).WithZone(tz)
Instant.FromDateTimeOffset(dto).InZone(tz)
这听起来像是一个制作应用程序,因此我现在花时间设置更新您自己的时区数据的能力。
有关如何使用NZD文件而不是DateTimeZoneProviders.Tzdb
中的嵌入式副本,请参阅the user guide。
一个好方法是构造函数注入IDateTimeZoneProvider
并将其注册到您选择的DI容器中。
请务必订阅Announcements list from IANA,以便了解何时发布新的TZDB更新。 Noda Time NZD文件通常会在很短的时间后发布。
或者,只要您了解更新后需要在您身边发生的事情(如果有的话),您就可以想象并向check for the latest .NZD file写一些内容并自动更新您的系统。 (当应用程序包含未来事件的安排时,这会发挥作用。)
WRT好友属性 - 是的,我同意他们是PITA。但不幸的是,EF目前还没有更好的方法,因为它不支持自定义类型映射。 EF6很可能不会有这种情况,但它会在aspnet/EntityFramework#242中跟踪EF7。
现在,尽管如此,你可能会稍微改变一下。我已经完成了上述工作,是的 - 它很复杂。简化的方法是:
根本不要在您的实体中使用Noda Time类型。只需使用DateTimeOffset
代替ZonedDateTime
。
仅在您执行应用程序逻辑的位置使用ZonedDateTime
和用户的时区。
这种方法的缺点是在你的领域中混淆了水域。有时,业务逻辑会进入服务而不是停留在它所属的实体中。或者,如果它确实存在于实体中,您现在必须将timeZoneId
参数传递给各种方法,否则您可能不会考虑它。有时这是可以接受的,但有时不是。这取决于它为你创造了多少工作。
最后,我将解释这一部分:
我们现在有一个基于Noda Time的应用程序。 ZonedDateTime对象可以更轻松地进行临时计算和时区驱动查询。
这是正确的假设吗?
是和否。在您将上述所有内容应用于您的应用程序之前,您可能希望与ZonedDateTime
隔离一些操作。
主要是,ZonedDateTime
确保在转换为其他类型和从其他类型进行转换时以及在进行涉及瞬时时间的数学运算时(使用Duration
个对象)正在考虑时区。
在处理日历时间时,它并没有真正帮助。例如,如果我想"添加一天" - 我需要考虑这是否意味着"添加24小时的持续时间"或者#34;添加一个日历日的时间段"。大多数日子都是相同的,但不是在包含DST过渡的日子里。在那里,它们的持续时间可以是23,23.5,24,24.5或25小时,具体取决于时区。 ZonedDateTime
不允许您直接添加Period
。相反,您必须获取LocalDateTime
,然后添加句点,然后重新应用时区以返回ZonedDateTime
。
所以 - 仔细考虑一下你是否需要以同样的方式使用它。如果您的应用程序逻辑严格遵循日历日,那么您可能会发现它最好用LocalDate
来编写。您可能需要通过各种属性和方法来实际使用该逻辑,但至少逻辑是以最纯粹的形式建模的。
希望这会有所帮助,希望这对其他读者来说是一个有用的帖子。祝你好运,随时请求我帮忙。