Глобально конвертировать UTC DateTimes в пользовательские локальные DateTimes

Я сохраняю все поля DateTime как время UTC. Когда пользователь запрашивает веб-страницу, я хотел бы воспользоваться предпочтительным местным часовым поясом (а не местным часовым поясом серверной машины) и автоматически отображать все поля DateTime во всех веб-формах в качестве локальных дат.

Конечно, я могу применить преобразование для каждого вызова DateTime.ToString() в каждой форме или реализовать некоторую вспомогательную утилиту, но это трудоемкая задача, а также некоторые сторонние компоненты, которые сложны для настройки с помощью пользовательских Шаблоны отображения DateTime.

По существу, я хотел бы заставить класс DateTime вести себя следующим образом:

from this moment on for this web request, 
whenever some code calls DateTime.ToString(), convert it to the local time 
        using the timezone offset given at the very beginning of the web request, 
but if possible, please keep .NET core library DateTime.ToString() calls intact 
       (I don't want to mess up event logging timestamps etc.)

Есть ли способ сделать это?

Кстати, я использую ASP.NET MVC 4, если это имеет значение.

Ответ 1

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

Вариант 1

  • Сначала определите, с каким типом данных часового пояса вы хотите работать. Доступны два разных типа: временные зоны Microsoft, к которым вы можете получить доступ, с классом TimeZoneInfo или часовыми поясами IANA/Olson, которые используются остальным миром. Подробнее читайте здесь. Моя рекомендация будет последней, используя реализацию, предоставленную NodaTime.

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

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

    • Возможно, вам захочется угадать часовой пояс по умолчанию, чтобы вы могли быть как можно ближе к точности, прежде чем выбрать из списка (или карты). Существует большая библиотека для этого jsTimeZoneDetect. Он будет допрашивать часы браузера и делать наилучшее предположение о том, какой часовой пояс он может быть. Это довольно хорошо, но это все еще просто предположение. Не используйте его вслепую - но используйте его для определения начальной точки. Обновление. Теперь вы можете сделать это с помощью moment.tz.guess(), в момент-время компонент момента .js.

  • Теперь, когда вы знаете часовой пояс пользователя, вы можете использовать это значение для преобразования ваших значений UTC DateTime в этот локальный часовой пояс. К сожалению, вы ничего не можете установить в потоке, который это сделает. При изменении системного часового пояса он является глобальным для всех процессов и потоков. Таким образом, у вас нет выбора, кроме как передать часовой пояс для каждого места, куда вы его отправляете. (Я считаю, что это был ваш главный вопрос.) См. Здесь почти дубликат.

  • Прежде чем преобразовать его в строку, вам также нужно знать локаль пользователя (которую вы можете получить из значения Request.UserLanguages ​​). Вы можете назначить его текущему потоку или передать его как параметр методу DateTime.ToString(). Это не делает никакого преобразования часового пояса - он просто гарантирует, что цифры находятся в правильном положении, используя правильные разделители и соответствующий язык для имен дней недели или месяцев.

Вариант 2

Не конвертируйте его в локальное время на сервере вообще.

  • Поскольку вы сказали, что работаете со значениями UTC, убедитесь, что их свойство .Kind Utc. Вероятно, вы должны сделать это, когда загружаете из своей базы данных, но если вам нужно это сделать вручную:

    myDateTime = DateTime.SpecifyKind(myDateTime, DateTimeKind.Utc);
    
  • Отправьте его обратно в браузер как чистый UTC в инвариантном формате, таком как ISO8601. Другими словами:

    myDateTime.ToString("o");  // example:  "2013-05-02T21:01:26.0828604Z"
    
  • Используйте JavaScript в браузере, чтобы разобрать его как UTC. Он автоматически подберет локальные настройки времени в браузере. Один из способов - использовать встроенный объект Date в JavaScript, например:

    var dt = new Date('2013-05-02T21:01:26.0828604Z');
    

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

    // pass the value from your server
    var m = moment('2013-05-02T21:01:26.0828604Z');
    
    // use one of the formats supported by moment.js
    // this is locale-specific "long date time" format.
    var s = m.format('LLLL');
    

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

Преимущество Варианта 2 состоит в том, что вы получаете браузер для выполнения некоторых работ для вас. Это лучший способ пойти, если вы отправляете необработанные данные, такие как вызов AJAX в WebAPI. Однако JavaScript знает только о UTC и локальном часовом поясе браузера. Так что это не так хорошо, если вам нужно преобразовать в другие зоны.

Вы также должны знать, что если вы выберете вариант № 2, вы можете столкнуться с недостатком в дизайне ECMAScript 5.1. Это вступает в игру, если вы работаете с датами, которые покрываются другим набором правил летнего времени, чем в настоящее время. Вы можете прочитать в этом вопросе и в моем блоге.

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

Ответ 2

Короткий ответ заключается в том, что вы не можете. HTTP не требует (или даже обеспечивает стандартный способ) для пользовательского агента (браузера) для предоставления локальной информации о времени или часовом поясе в HTTP-запросе.

Вам либо нужно

  • запросить у пользователя предпочтительный часовой пояс или
  • Javascript на стороне клиента сообщает об этом вам как-то (cookie? ajax? other?)

Имейте в виду, что решение JavaScript на стороне клиента не является идеальным. Javascript отключен (или отсутствует для некоторых браузеров). Возможно, Javascript не имеет доступа к информации о часовом поясе. Etc.