Как элегантно бороться с часовыми поясами - PullRequest
132 голосов
/ 28 сентября 2011

У меня есть веб-сайт, который находится в другом часовом поясе, чем пользователи, использующие приложение.В дополнение к этому пользователи могут иметь определенный часовой пояс.Мне было интересно, как другие пользователи и приложения SO подходят к этому?Наиболее очевидная часть заключается в том, что внутри БД дата / время хранятся в UTC.Находясь на сервере, все даты и время должны рассматриваться в UTC.Однако я вижу три проблемы, которые пытаюсь преодолеть:

  1. Получение текущего времени в формате UTC (легко решается с помощью DateTime.UtcNow).

  2. Извлечение даты / времени из базы данных и отображение их пользователю.Существует потенциально лотов вызовов для печати дат в разных представлениях.Я думал о некотором слое между представлением и контроллерами, которые могли бы решить эту проблему.Или с пользовательским методом расширения на DateTime (см. Ниже).Основным недостатком является то, что в каждом месте использования даты и времени в представлении должен быть вызван метод расширения!

    Это также добавит трудности при использовании чего-то вроде JsonResult.Вы больше не могли бы легко звонить Json(myEnumerable), это должно было бы быть Json(myEnumerable.Select(transformAllDates)).Может быть, AutoMapper мог бы помочь в этой ситуации?

  3. Получение ввода от пользователя (с локального на UTC).Например, отправка формы с датой потребует преобразования даты в UTC.Первое, что приходит на ум, - это создание собственного ModelBinder.

Вот расширения, которые я подумал использовать в представлениях:

public static class DateTimeExtensions
{
    public static DateTime UtcToLocal(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
    }

    public static DateTime LocalToUtc(this DateTime source, 
        TimeZoneInfo localTimeZone)
    {
        source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
        return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
    }
}

Я быдумаю, что работа с часовыми поясами была бы такой распространенной вещью, учитывая, что многие приложения в настоящее время основаны на облаке, где местное время сервера может сильно отличаться от ожидаемого часового пояса.

Это было элегантно решено раньше?Есть ли что-то, что я пропускаю?Идеи и мысли очень ценятся.

РЕДАКТИРОВАТЬ: Чтобы устранить некоторую путаницу, я подумал добавить еще несколько деталей.В настоящее время проблема заключается не в , как для хранения времени UTC в БД, а скорее в процессе перехода от UTC-> Local и Local-> UTC.Как указывает @Max Zerbini, очевидно разумно поместить код UTC-> Local в представление, но действительно ли использование DateTimeExtensions ответ?При получении ввода от пользователя имеет ли смысл принимать даты в качестве локального времени пользователя (поскольку именно это будет использовать JS), а затем использовать ModelBinder для преобразования в UTC?Часовой пояс пользователя хранится в БД и легко восстанавливается.

Ответы [ 6 ]

100 голосов
/ 28 сентября 2011

Не то, чтобы это была рекомендация, это скорее разделение парадигмы, но самый агрессивный способ, который я когда-либо видел, обработки информации о часовом поясе в веб-приложении (которое не является эксклюзивным для ASP.NET MVC) было следующим:

  • Все даты на сервере указаны в формате UTC.Это означает использование, как вы сказали, DateTime.UtcNow.

  • Старайтесь как можно меньше доверять клиенту даты передачи серверу.Например, если вам нужно «сейчас», не создавайте дату на клиенте, а затем передавайте ее на сервер.Либо создайте дату в вашем GET и передайте ее в ViewModel, либо на POST сделайте DateTime.UtcNow.

Пока что довольно стандартный тариф, но здесь все становится «интересным».

  • Если вам нужно принять дату отклиент, а затем используйте JavaScript, чтобы убедиться, что данные, которые вы отправляете на сервер, находятся в формате UTC.Клиент знает, в каком часовом поясе он находится, поэтому он может с разумной точностью конвертировать время в UTC.

  • При рендеринге представлений они использовали элемент HTML5 <time>, они никогда не рендеритсяdatetimes прямо в ViewModel.Это было реализовано как расширение HtmlHelper, что-то вроде Html.Time(Model.when).Он будет отображать <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Затем они будут использовать JavaScript для перевода времени UTC в местное время клиентов.Сценарий найдет все элементы <time> и использует свойство данных date-format для форматирования даты и заполнения содержимого элемента.

Таким образом, им никогда не приходилось отслеживатьиз, хранить или управлять часовым поясом клиентов.Сервер не заботился о том, в каком часовом поясе находится клиент, и не должен был делать никаких переводов часовых поясов.Он просто выплевывает UTC и позволяет клиенту преобразовать это в нечто разумное.Это легко сделать из браузера, потому что он знает, в каком часовом поясе он находится. Если клиент изменил свой часовой пояс, веб-приложение автоматически обновится.Единственное, что они сохранили, была строка формата datetime для локали пользователя.

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

13 голосов
/ 10 мая 2013

После нескольких отзывов, вот мое окончательное решение, которое я считаю чистым и простым и охватывает вопросы перехода на летнее время.

1 - Мы занимаемся преобразованием на уровне модели. Итак, в классе Model мы пишем:

    public class Quote
    {
        ...
        public DateTime DateCreated
        {
            get { return CRM.Global.ToLocalTime(_DateCreated); }
            set { _DateCreated = value.ToUniversalTime(); }
        }
        private DateTime _DateCreated { get; set; }
        ...
    }

2 - в глобальном помощнике мы делаем нашу пользовательскую функцию "ToLocalTime":

    public static DateTime ToLocalTime(DateTime utcDate)
    {
        var localTimeZoneId = "China Standard Time";
        var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
        return localTime;
    }

3 - Мы можем улучшить это, сохранив идентификатор часового пояса в каждом профиле пользователя, чтобы мы могли извлекать данные из класса пользователя вместо того, чтобы использовать постоянное «Китайское стандартное время»:

public class Contact
{
    ...
    public string TimeZone { get; set; }
    ...
}

4 - Здесь мы можем получить список часовых поясов, которые пользователь может выбрать из выпадающего списка:

public class ListHelper
{
    public IEnumerable<SelectListItem> GetTimeZoneList()
    {
        var list = from tz in TimeZoneInfo.GetSystemTimeZones()
                   select new SelectListItem { Value = tz.Id, Text = tz.DisplayName };

        return list;
    }
}

Итак, теперь в 9:25 в Китае, веб-сайт размещен в США, дата сохранена в UTC в базе данных, вот окончательный результат:

5/9/2013 6:25:58 PM (Server - in USA) 
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)

EDIT

Спасибо Мэтту Джонсону за то, что он указал на слабые места оригинального решения, и извините за удаление оригинального сообщения, но получил проблемы с получением правильного формата отображения кода ... оказалось, что редактор имеет проблемы со смешиванием "пуль "с" предварительным кодом ", поэтому я удалил буллы, и все было в порядке.

8 голосов
/ 28 сентября 2011

В разделе события на sf4answers пользователи вводят адрес для события, а также дату начала и дополнительную дату окончания. Это время преобразуется в datetimeoffset на сервере SQL, который учитывает смещение от UTC.

Это та же проблема, с которой вы сталкиваетесь (хотя вы придерживаетесь другого подхода, поскольку вы используете DateTime.UtcNow); у вас есть местоположение, и вам нужно перевести время из одного часового пояса в другой.

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

Во-вторых, при выполнении переводов, предполагая, что вы знаете местоположение / часовой пояс, в котором находится клиент, вы можете использовать общедоступную базу данных информационных часовых поясов для перевода времени из UTC в другой часовой пояс ( или триангулируйте, если хотите, между двумя часовыми поясами). Самое замечательное в базе данных tz (иногда ее называют база данных Олсона ) состоит в том, что она учитывает изменения часовых поясов в истории; получение смещения является функцией даты, на которую вы хотите получить смещение (просто посмотрите на Закон об энергетической политике 2005 года , который изменил даты вступления в силу летнего времени в США. ).

Имея базу данных в руках, вы можете использовать ZoneInfo (база данных tz / база данных Олсона) .NET API . Обратите внимание, что бинарного дистрибутива нет, вам придется загрузить последнюю версию и скомпилировать ее самостоятельно.

На момент написания этой статьи он в настоящее время анализирует все файлы в последнем дистрибутиве данных (на самом деле я запускал его для ftp: //elsie.nci.nih.gov/pub/tzdata2011k.tar. файл gz от 25 сентября 2011 г., в марте 2017 г. вы получите его по https://iana.org/time-zones или по ftp: //fpt.iana.org/tz/releases/tzdata2017a.tar. GZ ).

Таким образом, на sf4answers после получения адреса он геокодируется в комбинацию широта / долгота, а затем отправляется стороннему веб-сервису для получения часового пояса, соответствующего записи в базе данных tz. Отсюда время начала и окончания преобразуется в DateTimeOffset экземпляров с правильным смещением UTC, а затем сохраняется в базе данных.

Что касается работы с SO и веб-сайтами, это зависит от аудитории и того, что вы пытаетесь отобразить. Если вы заметили, большинство социальных сайтов (и SO, и раздел событий на sf4answers) отображают события в относительно времени, или, если используется абсолютное значение, обычно это UTC.

Однако, если ваша аудитория ожидает местное время, то использование DateTimeOffset вместе с методом расширения, который использует часовой пояс для преобразования, было бы просто замечательно; тип данных SQL datetimeoffset будет преобразован в .NET DateTimeOffset, который затем вы можете получить универсальное время для использования GetUniversalTime метода . Оттуда вы просто используете методы класса ZoneInfo для преобразования из UTC в местное время (вам нужно будет проделать небольшую работу, чтобы преобразовать его в DateTimeOffset, но это достаточно просто сделать).

Где сделать преобразование? Это стоимость, которую вы должны будете заплатить где-то , и нет "лучшего" способа. Я бы выбрал вид, хотя со смещением часового пояса как часть модели представления, представленной представлению. Таким образом, если требования к представлению изменяются, вам не нужно менять модель представления, чтобы учесть это изменение. Ваш JsonResult будет просто содержать модель с IEnumerable<T> и смещением.

На стороне ввода, используя модель переплета? Я бы сказал, ни за что. Вы не можете гарантировать, что все даты (сейчас или в будущем) должны быть преобразованы таким образом, это должно быть явной функцией вашего контроллера для выполнения этого действия. Опять же, если требования изменяются, вам не нужно настраивать один или несколько ModelBinder экземпляров для корректировки вашей бизнес-логики; - это бизнес-логика, что означает, что она должна быть в контроллере.

5 голосов
/ 28 сентября 2011

Это только мое мнение, я думаю, что приложение MVC должно хорошо отделить проблему представления данных от управления моделью данных. База данных может хранить данные во времени локального сервера, но на уровне представления обязан отображать дату и время с использованием часового пояса локального пользователя. Это кажется мне той же проблемой, что и I18N и формат чисел для разных стран. В вашем случае ваше приложение должно определить Culture и часовой пояс пользователя и изменить представление, отображающее другой текст, число и формат представления, но сохраненные данные могут иметь одинаковый формат.

1 голос
/ 07 октября 2011

Для вывода создайте шаблон отображения / редактирования, подобный этому

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

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

См. здесь и здесь для получения дополнительной информации о создании пользовательских шаблонов редактора.

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

Эта ссылка , мы надеемся, подтолкнет вас в правильном направлении, если вы захотите пойти по этому пути.

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

0 голосов
/ 05 октября 2011

Это, вероятно, кувалда, чтобы расколоть орех, но вы могли бы внедрить слой между уровнями пользовательского интерфейса и бизнес-уровня, который прозрачно преобразует время в местное время на графиках возвращаемых объектов и в UTC во входных параметрах даты и времени.

Я полагаю, что это может быть достигнуто с помощью PostSharp или некоторого инвертирования контейнера управления.

Лично я бы просто пошел с явным преобразованием ваших дат в UI ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...