Как элегантно работать с часовыми поясами

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

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

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

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

  3. Получение ввода от пользователя (Local to UTC). Например, для POST-формы с датой потребуется преобразовать дату в 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); } } 

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

Это было изящно разрешено раньше? Есть что-то, что мне не хватает? Идеи и мысли очень ценятся.

EDIT: Чтобы устранить некоторую путаницу, я подумал добавить несколько подробностей. Сейчас проблема заключается не в том, как хранить время UTC в db, это больше о процессе перехода от UTC-> Local и Local-> UTC. Как указывает @Max Zerbini, очевидно, что разумно поместить UTC-> Local code в представление, но использует ответ DateTimeExtensions действительно? При получении ввода от пользователя имеет смысл принимать даты как локальное время пользователя (так как это то, что JS будет использовать), а затем использовать ModelBinder для преобразования в UTC? Часовой пояс пользователя сохраняется в БД и легко извлекается.

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

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

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

До сих пор довольно стандартная плата за проезд, но здесь все становится «интересным».

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

  • При рендеринге представлений они использовали элемент HTML5 , они никогда не отображали данные непосредственно в ViewModel. Он был реализован как расширение HtmlHelper , что-то вроде Html.Time(Model.when) . Он будет отображать .

    Затем они будут использовать javascript для перевода времени UTC в локальное время клиента. Скрипт найдет все элементы и использует свойство данных формата даты для форматирования даты и заполнения содержимого элемента.

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

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

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

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

  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 – Мы можем улучшить это дальше, сохранив идентификатор часового пояса в каждом профиле пользователя, чтобы мы могли получить из classа пользователя вместо использования постоянного «Китайского стандартного времени»:

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

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

 public class ListHelper { public IEnumerable 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) 

РЕДАКТИРОВАТЬ

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

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

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

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

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

С помощью базы данных вы можете использовать базу данных ZoneInfo (firebase database tz / Olson) .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 вместе с методом расширения, использующим часовой пояс для конвертирования, будет очень хорошим; тип данных datetimeoffset данных SQL будет преобразовываться в .NET DateTimeOffset чего вы сможете получить универсальное время для использования метода GetUniversalTime . Оттуда вы просто используете методы classа ZoneInfo для преобразования из UTC в локальное время (вам нужно будет немного поработать, чтобы получить его в DateTimeOffset , но это достаточно просто сделать).

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

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

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

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

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

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

Подробнее о создании пользовательских шаблонов редакторов см. Здесь и здесь .

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

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

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

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

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

Лично я бы просто пошел с явным преобразованием ваших дат в пользовательский интерфейс …

  • linux конвертировать время (для разных часовых поясов) в UTC
  • iOS: конвертировать UTC NSDate в локальный часовой пояс
  • Как обрабатывать календарные часовые пояса с помощью Java?
  • Как использовать пользовательское время в браузере для проверки разницы во времени между клиентом и сервером
  • Как правильно установить JVM TimeZone
  • Как перевести между часовыми поясами Windows и IANA?
  • Как конвертировать дату с одного часового пояса в другой часовой пояс
  • Установите системный часовой пояс из .NET.
  • Преобразование временной зоны
  • Изменение часового пояса MySQL?
  • Преобразование дат UTC в другие часовые пояса
  • Давайте будем гением компьютера.