Noda Time 在现有 MVC5 应用程序中的实现策略

Posted

技术标签:

【中文标题】Noda Time 在现有 MVC5 应用程序中的实现策略【英文标题】:Implementation strategy for Noda Time in an existing MVC5 application 【发布时间】:2016-01-15 01:24:25 【问题描述】:

我们的应用程序是一个大型的 n 层 ASP.NET MVC 应用程序,它严重依赖于日期和(本地)时间。到目前为止,我们一直在为所有模型使用 DateTime,这很好用,因为多年来我们严格来说是一个国家网站,只处理一个时区。

现在情况发生了变化,我们正在向国际观众敞开大门。第一个想法是“哦,废话。我们需要重构整个解决方案!”

时区信息

我们打开 LinQPad 并开始绘制各种转换器以将常规 DateTime 对象转换为 DateTimeOffset 对象,基于基于用户的 TimeZone ID 值创建的 TimeZoneInfo 对象说用户的个人资料。

我们认为我们会将 models 中的所有 DateTime 属性更改为 DateTimeOffset 并完成它。毕竟,我们现在拥有了存储和显示用户本地日期和时间所需的所有信息。

sn-ps 的大部分代码都受到Rick Strahl's blog post 的启发。

NodaTime 和 DateTimeOffset

但后来我读到了Matt Johnson's excellent comment。他验证了我切换到 DateTimeOffset 的意图,声称:“DateTimeOffset 在 Web 应用程序中是必不可少的”

关于野田时间,马特说:

说到 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; 

本地日期时间到 NodaTime

为了防止出现大量重复代码,我们的计划是创建自定义 ModelBinders,将本地 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

我们是不是想多了?

在数据库中存储 DateTimeOffset

我们将使用concept of Buddy properties。老实说,我不是这个的忠实粉丝,因为它造成的混乱。新开发者可能会因为我们有不止一种方法来保存创建日期这一事实而苦恼。

非常欢迎就如何改进这一点提出建议。我已阅读有关从 IntelliSense 隐藏属性以将真实属性设置为 private 的 cmets。

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 对象可以更轻松地进行临时计算和时区驱动的查询。

这是一个正确的假设吗?

【问题讨论】:

现在正在消化您的帖子......很快就会回复...... :) 酷 :) 谢谢马特。这将是一个影响很大的重构,所以只是想在开始之前验证我们的计划。 重要问题 - 什么样的字符串输入期望作为参数接收到您的控制器?国际标准化组织?带偏移还是平原?世界标准时间?变化?请给出示例值。谢谢。 我们得到本地日期时间,没有 UTC 信息。 例如“2015-10-16 20:40:00” 【参考方案1】:

首先,我必须说我印象深刻!这是一篇写得很好的帖子,您似乎已经探索了围绕这个主题的许多问题。

你的方法很好。但是,我将提供以下内容供您考虑作为改进。

模型绑定器可以改进。

我将其命名为 ZonedDateTimeModelBinder,因为您正在应用它来创建 ZonedDateTime 值。

您需要使用bindingContext 来获取值,而不是期望输入始终位于request.Form.Get("DateTime") 中。你可以在the WebAPI model binder I wrote for LocalDate 中看到一个例子。 MVC 模型绑定器类似。

您还将在该示例中看到我如何使用 Noda Time 的解析功能而不是 DateTime.Parse。您可能会考虑使用LocalDateTimePattern 来做您自己的事情。

请确保您了解AtLeniently 的工作原理,并且我们已在即将发布的 2.0 版本中更改了其行为(有充分的理由)。请参阅the migration guide 底部的“Lenient resolver changes”。如果这在您的域中很重要,您现在可以考虑通过实施自己的解析器来使用新行为。

您可能会认为,在某些情况下,当前用户的时区不是您当前正在使用的数据的时区。也许管理员正在处理其他用户的数据。因此,您可能需要将时区 ID 作为参数的重载。

对于一般情况,您可以尝试全局注册模型绑定器,这将节省您在控制器上的一些击键:

  ModelBinders.Binders.Add(typeof(ZonedDateTime), new ZonedDateTimeModelBinder());

如果有参数要传递,你仍然可以使用属性化方式。

在代码的底部,ZonedDateTime.FromDateTimeOffset(dto).ToInstant().InZone(tz) 很好,但可以用更少的代码完成。这些中的任何一个都是等效的:

ZonedDateTime.FromDateTimeOffset(dto).WithZone(tz) Instant.FromDateTimeOffset(dto).InZone(tz)

这听起来像是一个生产应用程序,因此我现在需要时间来设置更新您自己的时区数据的能力。

请参阅the user guide,了解如何使用新西兰元文件而不是DateTimeZoneProviders.Tzdb 中的嵌入副本。

一个好的方法是构造函数注入 IDateTimeZoneProvider 并将其注册到您选择的 DI 容器中。

请务必订阅Announcements list from IANA,以便了解新的 TZDB 更新何时发布。 Noda Time NZD 文件通常会在很短的时间之后出现。

或者,只要您了解更新后您身边需要发生什么(如果有的话),您也可以花点时间向check for the latest .NZD file 写一些东西并自动更新您的系统。 (当应用程序包含未来事件的安排时,这就会发挥作用。)

WRT 好友属性 - 是的,我同意它们是 PITA。但不幸的是,EF 目前没有更好的方法,因为它不支持自定义类型映射。 EF6 可能永远不会有这个,但它正在 aspnet/EntityFramework#242 中为 EF7 跟踪。

更新 - 在 EF Core 中,您现在不能使用 Value Converters 在实体模型中支持 Noda Time 数据类型。

现在,说了这么多,您的处理方式可能会略有不同。我已经完成了上述操作,是的 - 这很复杂。一种简化的方法是:

根本不要在您的实体中使用 Noda Time 类型。只需使用DateTimeOffset 而不是ZonedDateTime

仅在您执行应用程序逻辑时才涉及ZonedDateTime 和用户的时区。

这种方法的缺点是会使您的域变得混乱。有时业务逻辑会进入服务而不是停留在它所属的实体中。或者,如果它确实保留在实体中,您现在必须将 timeZoneId 参数传递给您可能不会考虑的各种方法。有时这是可以接受的,但有时不是。这取决于它为您创造了多少工作。

最后,我会谈谈这部分:

我们现在有了一个基于 Noda Time 的应用程序。 ZonedDateTime 对象可以更轻松地进行临时计算和时区驱动的查询。

这是一个正确的假设吗?

是和不是。在您将上述所有内容应用到您的应用程序之前,您可能想尝试一些与ZonedDateTime 隔离的操作。

主要是,ZonedDateTime 确保在与其他类型之间转换以及在执行涉及瞬时时间的数学运算时考虑时区(使用Duration 对象)。

在使用日历时间时,它并没有真正的帮助。例如,如果我想“添加一天” - 我需要考虑这意味着“添加 24 小时的持续时间”还是“添加一个日历日的时间段”。对于大多数日子来说,这将是同样的事情,但在包含 DST 转换的日子里则不然。在那里,它们的持续时间可能是 23、23.5、24、24.5 或 25 小时,具体取决于时区。 ZonedDateTime 不会让你直接添加Period。相反,您必须获取LocalDateTime,然后添加句点,然后重新应用时区以返回ZonedDateTime

所以 - 请仔细考虑是否在任何地方都需要相同的方式。如果您的应用程序逻辑严格地与日历日有关,那么您可能会发现最好完全按照LocalDate 编写。您可能必须通过各种属性和方法才能真正使用该逻辑,但至少该逻辑是以其最纯粹的形式建模的。

希望这会有所帮助,并希望这对其他读者有用。祝你好运,欢迎随时致电我寻求帮助。

【讨论】:

现在这是一个答案。我很感激这个马特,我会用整个周末来试验你的建议! 离题:但觉得只有让您知道您的建议有所作为才合适。谢谢! @Matt You 说,“你可能会喜欢并写一些东西来检查最新的 .NZD 文件并自动更新你的系统。” 是检查的最佳实践每天更新文件,或者甚至更频繁,因为偏移量等可能会根据当地规则每天更改? “最佳实践”是一个过度使用的术语……哈哈。当然——你可以通过编程来完成。但至少,我会订阅iana.org/time-zones 的“公告”邮件列表,以便您知道何时发布更改。 Noda Time 的.nzd 文件等下游版本紧随其后。

以上是关于Noda Time 在现有 MVC5 应用程序中的实现策略的主要内容,如果未能解决你的问题,请参考以下文章

如何在 WCF 中将 Noda Time(或任何第三方类型)对象作为参数传递?

将 ASP.NET MVC5 身份验证添加到现有项目

现有 MVC 5 站点上的 Piranha CMS

现有数据库上的 AspNet Identity ApplicationDbContext

如何在现有数据库中创建 ASP.Net Identity 表?

MVC5 Identity + EntityFramework 中的多个上下文