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

Fre*_*III 17 c# timezone datetime datetimeoffset nodatime

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

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

的TimeZoneInfo

我们打开了LinQPad并开始绘制各种转换器,根据基于用户的配置文件中用户的TimeZone ID值创建的对象,将常规DateTime对象转换为DateTimeOffset对象TimeZoneInfo.

我们计算过,我们可以将所有DateTime属性的车型进入DateTimeOffset,并用它做.毕竟,我们现在拥有了存储和显示用户本地日期和时间所需的所有信息.

许多代码片段都受到Rick Strahl关于这一主题的博客文章的启发.

NodaTime和DateTimeOffset

但后来我读到了马特约翰逊的出色评论.他验证了我打算转而DateTimeOffset声称:"DateTimeOffset在Web应用程序中至关重要".

关于Noda Time,Matt说:

说到Noda Time,我不同意你的意见,你必须更换整个系统中的所有内容.当然,如果你这样做,你将有更少的机会犯错误,但你肯定可以在有意义的地方使用Noda Time.我个人致力于使用IANA时区进行时区转换的系统(例如"America/Los_Angeles"),但跟踪DateTime和DateTimeOffset类型中的所有其他内容.实际上很常见的是Noda Time在应用程序逻辑中广泛使用,但完全脱离了DTO和持久层.在某些技术中,如实体框架,如果您愿意,则无​​法直接使用Noda Time - 因为没有地方可以将其连接起来.

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

我们的计划好还是坏?

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

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

确定正确的关键因素ZonedDateTimeTimeZoneIdUser模型中的属性.

public class ApplicationUser : IdentityUser
{
    [Required]
    public string TimezoneId { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

本地DateTime到NodaTime

为了防止大量重复代码,我们的计划是创建将本地转换DateTime为的自定义ModelBinder 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我们是在过度思考这个吗?

将DateTimeOffset存储在DB中

我们将使用Buddy属性概念.说实话,我不是一个忠实的粉丝,因为它造成了混乱.新开发人员可能会因为我们有多种方法来保存创建日期这一事实.

关于如何改善这一点的建议非常受欢迎.我已阅读有关将属性从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);
    }
}
Run Code Online (Sandbox Code Playgroud)

两者之间的一切

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

这是正确的假设吗?

Mat*_*int 12

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

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

  • 模型绑定器可以改进.

    • 我会命名ZonedDateTimeModelBinder,因为你正在应用它来创造ZonedDateTime价值.

    • 你会想要使用它bindingContext来获取值,而不是期望输入始终在 request.Form.Get("DateTime").您可以在我编写的WebAPI模型绑定LocalDate器中看到此示例.MVC模型绑定器是类似的.

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

    • 确保你了解它的AtLeniently工作原理,以及我们为即将发布的2.0版本改变了它的行为(有充分的理由).请参阅迁移指南底部的"Lenient解析器更改" .如果这在您的域中很重要,您可能需要考虑通过实施自己的解析器来使用今天的新行为.

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

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

    ModelBinders.Binders.Add(typeof(ZonedDateTime), new ZonedDateTimeModelBinder());
    
    Run Code Online (Sandbox Code Playgroud)

    如果有要传递的参数,您始终可以使用归因方式.

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

    • ZonedDateTime.FromDateTimeOffset(dto).WithZone(tz)
    • Instant.FromDateTimeOffset(dto).InZone(tz)
  • 这听起来像是一个生产应用程序,因此我现在花时间设置更新您自己的时区数据的能力.

    • 请参阅用户指南,了解如何使用NZD文件而不是嵌入式副本DateTimeZoneProviders.Tzdb.

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

    • 请务必订阅IANA公告列表,以便了解何时发布新的TZDB更新.Noda Time NZD文件通常会在很短的时间后出现.

    • 或者,只要您了解更新后需要在您身边发生的事情(如果有的话),您就可以得到想象并写一些东西来检查最新的.NZD文件并自动更新您的系统.(当应用程序包含未来事件的安排时,这会发挥作用.)

  • WRT好友属性 - 是的,我同意他们是PITA.但遗憾的是,EF目前没有更好的方法,因为它不支持自定义类型映射.EF6很可能永远不会有这种情况,但它正在aspo / EntityFramework#242中跟踪EF7.

现在,尽管如此,你可能会稍微改变一下.我已经完成了上述工作,是的 - 这很复杂.简化的方法是:

  • 根本不要在您的实体中使用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.您可能必须通过各种属性和方法来实际使用该逻辑,但至少逻辑是以其最纯粹的形式建模的.

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

  • 偏离主题:但觉得让你知道你的建议有所不同是恰当的.谢谢! (2认同)