Стратегия реализации для Noda Time в существующем приложении MVC5

Наше приложение представляет собой большое приложение ASP.NET MVC n-уровня, которое сильно зависит от дат и (локальных) времен. До сих пор мы использовали DateTime для всех наших моделей, которые отлично работали, потому что годами мы были строго национальным сайтом, работающим с одним часовым поясом.

Теперь все изменилось, и мы открываем двери для международной аудитории. Первая мысль: "О, дерьмо. Нам нужно реорганизовать все наше решение!"

TimeZoneInfo

Мы открыли LinQPad и начали рисовать различные конвертеры для преобразования регулярных объектов DateTime в объекты DateTimeOffset на основе объекта TimeZoneInfo, который был создан на основе значения User TimeZone User из указанного профиля пользователя.

Мы решили, что мы изменим все свойства DateTime в моделях на DateTimeOffset и покончим с этим. В конце концов, у нас появилась вся информация, необходимая для хранения и отображения локальной даты и времени пользователя.

Значительная часть фрагментов кода была вдохновлена ​​сообщение в блоге Рика Страгла по этому вопросу.

NodaTime и DateTimeOffset

Но затем я прочитал Отличный комментарий Мэтта Джонсона. Он подтвердил мое намерение перейти на DateTimeOffset, заявив: "DateTimeOffset имеет важное значение в веб-приложении".

Что касается времени Ноды, Мэтт говорит:

Говоря об Noda Time, я не соглашусь с вами, что вам нужно заменить все на всей вашей системе. Конечно, если вы это сделаете, у вас будет гораздо меньше возможностей совершать ошибки, но вы, безусловно, можете просто использовать Noda Time там, где это имеет смысл. Я лично работал над системами, которые необходимы для преобразования часовых поясов с использованием часовых поясов IANA (например, America/Los_Angeles), но отслеживал все остальное в типах DateTime и DateTimeOffset. На самом деле довольно часто можно увидеть, что Noda Time широко используется в логике приложений, но полностью покинул уровни DTO и persistence. В некоторых технологиях, таких как Entity Framework, вы не могли напрямую использовать Noda Time, если хотите - потому что там нет места для его подключения.

Это могло быть прямо направлено на нас, поскольку мы находимся в этом точном сценарии прямо сейчас, включая наш выбор для использования часовых поясов IANA.

Наш план, хороший или плохой?

Наша главная цель - создать наименее сложный рабочий процесс для работы с датами и временем в разных часовых поясах. Избегайте расчетов часового пояса как можно больше в наших Сервисах, Хранилищах и Контроллерах.

Вкратце, план состоит в том, чтобы принимать локальные даты и время из нашего front-end, как можно скорее конвертировать их в 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 в БД

Мы будем использовать концепцию свойств свойств 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 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

Во-первых, я должен сказать, что я впечатлен! Это очень хорошо написанная запись, и вы, похоже, изучили многие проблемы, связанные с этим вопросом.

Ваш подход хорош. Тем не менее, я предложу следующее для вас, чтобы рассмотреть как улучшения.

  • Модельное связующее может быть улучшено.

    • Я бы назвал его ZonedDateTimeModelBinder, так как вы применяете его для создания значений ZonedDateTime.

    • Вы хотите использовать bindingContext, чтобы получить значение, а не ожидать, что вход всегда будет находиться в request.Form.Get("DateTime"). Вы можете увидеть пример этого в привязке модели WebAPI, которую я написал для LocalDate. Модельные вставки MVC аналогичны.

    • В этом примере вы также увидите, как я использую возможности Node Time parsing вместо DateTime.Parse. Вы можете подумать о том, чтобы сделать что-то, что у вас, используя LocalDateTimePattern.

    • Убедитесь, что вы понимаете, как работает AtLeniently, а также что мы изменили его поведение для предстоящей версии 2.0 (по уважительной причине). См. "Изменения преобразователя Lenient" внизу руководство по миграции. Если это имеет значение в вашем домене, вы можете рассмотреть возможность использования нового поведения сегодня, внедряя свой собственный резольвер.

    • Вы могли бы подумать, что могут быть контексты, в которых текущий часовой пояс пользователя не является таковым для данных, с которыми вы работаете в настоящее время. Возможно, администратор работает с некоторыми другими пользовательскими данными. Поэтому вам может потребоваться перегрузка, которая принимает идентификатор часового пояса в качестве параметра.

  • В обычном случае вы можете попробовать зарегистрировать привязку модели по всему миру, что позволит сэкономить вам несколько нажатий клавиш на ваших контроллерах:

    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.

    • Хорошим подходом является конструктор-инъекция IDateTimeZoneProvider и зарегистрировать его в контейнере DI по вашему выбору.

    • Обязательно подпишитесь на Список объявлений из IANA, чтобы вы знали, когда публикуются новые обновления TZDB. Файлы Noda Time NZD обычно следуют через короткое время.

    • Или вы могли бы придумать и написать что-то проверить последний файл .NZD и автоматически обновить свою систему, так как если вы понимаете, что (если что-либо) должно произойти на вашей стороне после обновления. (Это входит в игру, когда приложение включает планирование будущих событий.)

  • Свойства приятеля WRT - Да, я согласен, что они PITA. Но, к сожалению, в настоящее время EF не имеет лучшего подхода, поскольку он не поддерживает сопоставления пользовательских типов. EF6, вероятно, никогда не будет этого, но он отслеживается в aspnet/EntityFramework # 242 для EF7.

Теперь, со всем сказанным, вы можете идти о вещах несколько иначе. Я сделал это, и да - это сложно. Упрощенный подход:

  • Не используйте типы времени Noda в ваших сущностях вообще. Просто используйте DateTimeOffset вместо ZonedDateTime.

  • Вовлекайте ZonedDateTime и часовой пояс пользователя только в том месте, где вы выполняете логику приложения.

Недостатком такого подхода является то, что он загрязняет воды в отношении вашего домена. Иногда бизнес-логика находит свой путь в сервисах, а не остается в сущности, где она принадлежит. Или, если он остается в сущности, теперь вам нужно передать параметр timeZoneId различным методам, в которых вы, возможно, и не думали об этом иначе. Иногда это приемлемо, но иногда нет. Это зависит от того, сколько работы оно создает для вас.

Наконец, я рассмотрю эту часть:

Теперь у нас есть приложение Noda Time. Объект ZonedDateTime упрощает выполнение специальных вычислений и запросов с часовым поясом.

Это правильное предположение?

Да и нет. Прежде чем зайти слишком глубоко, применив все вышеперечисленное к вашему приложению, вы можете попробовать несколько операций в изоляции с помощью ZonedDateTime.

В первую очередь ZonedDateTime обеспечивает учет часового пояса при преобразовании в другие типы и из других, а также при выполнении математических операций, которые включают мгновенное время (с использованием объектов Duration).

Там, где это действительно не помогает, приходится работать с календарным временем. Например, если я хочу "добавить один день" - мне нужно подумать о том, означает ли это "добавить продолжительность 24 часа" или "добавить период в один календарный день". В течение большинства дней это будет одно и то же, но не в дни, содержащие переходы DST. Там они могут быть 23, 23,5, 24, 24,5 или 25 часов в зависимости от часового пояса. ZonedDateTime не позволит вам напрямую добавить Period. Вместо этого вы должны получить LocalDateTime, затем добавить период, а затем повторно применить часовой пояс, чтобы вернуться к ZonedDateTime.

Итак, подумайте о том, нужно ли вам одинаково везде или нет. Если ваша логика приложения строго о календарных днях, вы можете найти ее лучше всего, написанную исключительно с точки зрения LocalDate. Возможно, вам придется работать через различные свойства и методы, чтобы фактически использовать эту логику, но по крайней мере логика моделируется в самой чистой форме.

Надеюсь, что это поможет, и, надеюсь, это будет полезная статья для других читателей. Удачи, и не стесняйтесь обращаться ко мне за помощью.