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

Implementation strategy for Noda Time in an existing MVC5 application

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

现在情况发生了变化,我们正在为国际观众敞开大门。第一个想到的是"Oh, Crap. We need to refactor our entire solution!"

时区信息

我们打开 LinQPad 并开始草拟各种 转换器 ,以根据 TimeZoneInfo 对象将常规 DateTime 对象转换为 DateTimeOffset 对象它是根据所述用户个人资料中的用户时区 ID 值创建的。

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

许多代码片段的灵感来自 Rick Strahl's blog post 关于该主题的内容。

NodaTime 和 DateTimeOffset

但后来我读了 Matt Johnson's excellent comment。他证实了我切换到 DateTimeOffset 的意图并声称:"DateTimeOffset is essential in a web application"

关于 Noda Time,Matt 说:

Speaking of Noda Time, I'll disagree with you that you have to replace everything throughout your system. Sure, if you do, you'll have a lot less opportunity to make mistakes, but you certainly can just use Noda Time where it makes sense. I've personally worked on systems that needed to do time zone conversions using IANA time zones (ex. "America/Los_Angeles"), but tracked everything else in DateTime and DateTimeOffset types. It's actually quite common to see Noda Time used extensively in application logic, but left completely out of the DTOs and persistence layers. In some technologies, like Entity Framework, you couldn't use Noda Time directly if you wanted to - because there's no where to hook it up.

这可能直接针对我们,因为我们现在正处于那种情况,包括我们选择使用 IANA 时区。

我们的计划,好还是坏?

我们的主要目标是创建最简单的工作流 来处理不同时区的日期和时间。在我们的服务、存储库和控制器中尽可能避免时区计算。

简而言之,计划是从我们的前端接受本地日期和时间,尽快将它们转换为 ZonedDateTime,并尽可能晚地将它们转换为 DateTimeOffset,就在将信息保存到数据库之前。

决定正确ZonedDateTime的关键因素是User模型中的TimeZoneId属性。

public class ApplicationUser : IdentityUser
{
    [Required]
    public string TimezoneId { get; set; }
}

本地 DateTime 到 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.

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

这是一个正确的假设吗?

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

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

  • 模型绑定器可以改进。

    • 我将其命名为 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 底部的“宽松解析器更改”。如果这在您的域中很重要,您可能希望立即考虑通过实施您自己的解析器来使用新行为。

    • 您可能会认为,在某些情况下,当前用户的时区可能与您当前使用的数据所在的时区不一致。也许管理员正在处理其他用户的数据。因此,您可能需要一个将时区 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 关于如何使用 NZD 文件而不是 DateTimeZoneProviders.Tzdb 中的嵌入副本。

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

  • 请务必订阅 Announcements list from IANA,以便您知道何时发布新的 TZDB 更新。 Noda Time NZD 文件通常紧随其后。

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

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

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

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

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

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

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

最后,我将解决这一部分:

We now have a Noda Time based application. The ZonedDateTime object makes it easier to do ad-hoc calculations and time zone driven queries.

Is this a correct assumption?

是也不是。在您深入了解将上述所有内容应用到您的应用程序之前,您可能想单独尝试一些使用 ZonedDateTime.

的操作

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

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

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

希望这对您有所帮助,并希望这对其他读者有用 post。祝你好运,随时向我寻求帮助。