我们的应用程序是一个大型的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对象,可以更轻松地进行临时计算和时区驱动的查询。

这是正确的假设吗?

最佳答案

首先,我必须说我印象深刻!这是一篇写得很好的文章,您似乎已经探讨了与此主题相关的许多问题。
您的方法很好。但是,我将提供以下内容供您考虑作为改进。

  • 可以改进模型绑定(bind)程序。
  • 我将其命名为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解析器更改”。如果这在您的域中很重要,则可能需要考虑通过实现自己的解析程序来使用新行为。
  • 您可能会考虑在某些上下文中当前用户的时区不是您当前正在使用的数据所在的时区。管理员可能正在处理其他用户的数据。因此,您可能需要将时区ID作为参数的重载。

  • 对于通常的情况,您可以尝试在全局范围内注册模型绑定(bind)程序,这将节省一些 Controller 上的按键操作:
      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
  • 一个好的方法是构造函数注入(inject)IDateTimeZoneProvider并将其注册到您选择的DI容器中。
  • 确保订阅Announcements list from IANA,以便您知道何时发布新的TZDB更新。 Noda Time NZD文件通常在很短一段时间后才会显示。
  • 或者,只要您了解更新后需要发生的事情(如果有的话),您就可以幻想并向check for the latest .NZD file写一些东西并自动更新系统。 (当应用程序包含 future 事件的调度时,这才起作用。)
  • WRT好友属性-是的,我同意它们是PITA。但是不幸的是,EF目前没有更好的方法,因为它不支持自定义类型映射。 EF6可能永远不会有,但是EF7在aspnet/EntityFramework#242中被跟踪。
  • 更新-在EF Core中,您现在不能在实体模型中使用Value Converters支持Noda Time数据类型。


  • 现在,尽管如此,您在处理事情上可能会略有不同。我已经完成了上述操作,是的-非常复杂。一种简化的方法是:
  • 根本不要在实体中使用Noda Time类型。只需使用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/

    10-10 14:57