Подтвердить что ты не робот

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

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

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

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

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

  • Получение ввода от пользователя (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);
    }
}

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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)

ИЗМЕНИТЬ

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

Ответ 4

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

Ответ 5

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

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

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

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

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

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

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

Ответ 6

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

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

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