我们的应用程序是一个大型的n层ASP.NET MVC应用程序,它很大程度上依赖于日期和(本地)时间。到现在为止,我们一直在对所有模型使用DateTime
,效果很好,因为多年来我们严格来说是一个全国性网站,只处理一个时区。
现在情况已经改变,我们正在为国际观众打开大门。第一个想法是“哦,废话。我们需要重构我们的整个解决方案!”
时区信息
我们打开了LinQPad,并开始草绘各种转换器,以根据基于来自所述用户配置文件的用户的TimeZone ID值创建的DateTime
对象,将常规DateTimeOffset
对象转换为TimeZoneInfo
对象。
我们认为可以将模型中的所有DateTime
属性更改为DateTimeOffset
并完成此操作。毕竟,我们现在拥有了存储和显示用户的本地日期和时间所需的所有信息。
许多代码片段都是受Rick Strahl's blog post启发的。
NodaTime和DateTimeOffset
但是后来我读了Matt Johnson's excellent comment。他证实了我打算切换到DateTimeOffset
的意图,声称:“DateTimeOffset在Web应用程序中至关重要”。
关于Noda Time,Matt说:
这可能直接针对我们,因为我们目前正处于这种情况下,包括我们选择使用IANA时区。
我们的计划好不好?
我们的主要目标是创建最简单的工作流,以处理各种时区中的日期和时间。尽量避免在我们的服务,存储库和 Controller 中进行时区计算。
简而言之,计划是从前端接受本地日期和时间,并在将信息保存到数据库之前,尽快将其转换为ZonedDateTime
,并尽可能晚地将其转换为DateTimeOffset
。
确定正确的ZonedDateTime
的关键因素是用户模型中的TimeZoneId
属性。
public class ApplicationUser : IdentityUser
{
[Required]
public string TimezoneId { get; set; }
}
本地DateTime到NodaTime
为了防止大量重复代码,我们的计划是创建自定义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;
}
}
我们可以使用这些模型绑定(bind)器来填充 Controller 。
[HttpPost]
[Authorize]
public ActionResult SimpleDateTime([ModelBinder(typeof (LocalDateTimeModelBinder))] ZonedDateTime dateTime)
{
// Do stuff with the ZonedDateTime object
}
我们是否对此考虑过度?
在数据库中存储DateTimeOffset
我们将使用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对象,可以更轻松地进行临时计算和时区驱动的查询。
这是正确的假设吗?
最佳答案
首先,我必须说我印象深刻!这是一篇写得很好的文章,您似乎已经探讨了与此主题相关的许多问题。
您的方法很好。但是,我将提供以下内容供您考虑作为改进。
ZonedDateTimeModelBinder
,因为您正在将其应用于创建ZonedDateTime
值。bindingContext
来获取值,而不是期望输入始终在request.Form.Get("DateTime")
中。您可以在the WebAPI model binder I wrote for LocalDate
中看到一个示例。 MVC模型资料夹是相似的。DateTime.Parse
。您可能会考虑使用LocalDateTimePattern
做一些您自己的事情。AtLeniently
的工作原理,并且我们已经为即将发布的2.0版本更改了它的行为(有充分的理由)。请参阅the migration guide底部的“Lenient解析器更改”。如果这在您的域中很重要,则可能需要考虑通过实现自己的解析程序来使用新行为。 ModelBinders.Binders.Add(typeof(ZonedDateTime), new ZonedDateTimeModelBinder());
如果有参数要传递,您仍然可以始终使用属性方式。ZonedDateTime.FromDateTimeOffset(dto).ToInstant().InZone(tz)
很好,但是可以用更少的代码完成。这些是等效的:ZonedDateTime.FromDateTimeOffset(dto).WithZone(tz)
Instant.FromDateTimeOffset(dto).InZone(tz)
DateTimeZoneProviders.Tzdb
中的嵌入式副本的信息,请参见the user guide。IDateTimeZoneProvider
并将其注册到您选择的DI容器中。现在,尽管如此,您在处理事情上可能会略有不同。我已经完成了上述操作,是的-非常复杂。一种简化的方法是:
DateTimeOffset
而不是ZonedDateTime
即可。ZonedDateTime
和用户的时区。这种方法的缺点是使您的 Realm 陷入困境。有时,业务逻辑会找到进入服务的方式,而不是停留在其所属的实体中。或者,如果它确实存在于实体中,则现在必须将
timeZoneId
参数传递给您可能不会考虑的各种方法。有时这是可以接受的,但有时却不能。这仅取决于它为您创造了多少工作。最后,我将介绍这一部分:
是的,没有。在将上述所有内容应用到应用程序之前,您可能需要尝试使用
ZonedDateTime
单独进行一些操作。首先,
ZonedDateTime
可确保在与其他类型进行相互转换以及进行涉及瞬时时间的数学运算(使用Duration
对象)时考虑时区。真正没有帮助的地方是使用日历时间。例如,如果我想“增加一天”-我需要考虑这是否意味着“增加24小时的持续时间”或“增加一个日历日的时间”。对于大多数日子来说,这是相同的,但对于包含DST转换的日子却不是。在那里,根据时区的不同,持续时间可能为23、23.5、24、24.5或25小时。
ZonedDateTime
不允许您直接添加Period
。相反,您必须获取LocalDateTime
,然后添加时间段,然后重新应用时区以返回ZonedDateTime
。因此-请仔细考虑您是否在各处都需要它。如果您的应用程序逻辑严格来说是日历日,那么您可能会发现最好用
LocalDate
专门编写它。您可能必须研究各种属性和方法才能实际使用该逻辑,但是至少该逻辑是以其最纯粹的形式建模的。希望对您有所帮助,并希望这对其他读者是有用的。祝您好运,请随时致电我寻求帮助。
关于c# - 现有MVC5应用程序中Noda Time的实现策略,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/33167972/