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

时间:2015-10-16 10:15:41

标签: c# datetime timezone datetimeoffset nodatime

我们的应用程序是一个很大的n层ASP.NET MVC应用程序,它严重依赖于Dates和(local)Times。到目前为止,我们已经将DateTime用于我们所有的模型,这些模型运行良好,因为多年来我们严格地是一个国家网站,处理单个时区。

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

的TimeZoneInfo

我们打开了LinQPad并开始草绘各种转换器,根据基于创建的DateTime对象将常规DateTimeOffset对象转换为TimeZoneInfo个对象来自所述用户个人资料的用户的TimeZone ID值。

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

许多代码段都受Rick Strahl's blog post关于此主题的启发。

NodaTime和DateTimeOffset

但后来我读了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; }
}

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

我们可以使用这些模型粘合剂来丢弃控制器。

[HttpPost]
[Authorize]
public ActionResult SimpleDateTime([ModelBinder(typeof (LocalDateTimeModelBinder))] ZonedDateTime dateTime)
{
   // Do stuff with the ZonedDateTime object
}

我们是否过度思考?

将DateTimeOffset存储在DB

我们将使用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对象使得进行临时计算和时区驱动查询变得更加容易。

这是正确的假设吗?

1 个答案:

答案 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来编写。您可能需要通过各种属性和方法来实际使用该逻辑,但至少逻辑是以最纯粹的形式建模的。

希望这会有所帮助,希望这对其他读者来说是一个有用的帖子。祝你好运,随时请求我帮忙。