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

140

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

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

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

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

  3. Получение ввода от пользователя (локально по 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 в базе данных, а в процессе перехода от UTC-> Local и Local-> UTC. Как указывает @Max Zerbini, очевидно, разумно поместить код UTC-> Local в представление, но DateTimeExtensionsдействительно ли используется ответ? При получении ввода от пользователя имеет ли смысл принимать даты как местное время пользователя (поскольку это то, что будет использовать JS), а затем использовать a ModelBinderдля преобразования в UTC? Часовой пояс пользователя хранится в БД и его легко получить.

Безоблачное Небо
источник
3
Вы можете сначала прочитать этот отличный пост ... Лучшие практики
перехода на
@dodgy_coder - это всегда была отличная ссылка на ресурсы для часовых поясов. Однако на самом деле это не решает ни одной из моих проблем (особенно относящихся к MVC). Спасибо хоть.
TheCloudlessSky
code.google.com/p/noda-time может быть полезно
Арнис Лапса
Любопытно, какое решение вы выбрали. Сам столкнулся с подобным решением. Хороший вопрос.
Шон
@Sean - решение до сих пор не было элегантным (поэтому я еще не получил ответа). Было много ручных накладных расходов на печать даты / времени и их ручное преобразование обратно с расширением ModelBinder.
TheCloudlessSky

Ответы:

106

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

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

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

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

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

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

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

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

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

Дж. Холмс
источник
Спасибо за ваш ответ. При получении ввода даты от пользователя (например, при планировании встречи) предполагается, что этот ввод находится в их текущем часовом поясе? Что вы думаете о ModelBinder, выполняющем это преобразование перед входом в действие (см. Мои комментарии к @casperOne. Кроме того, <time>идея довольно крутая. Единственным недостатком является то, что это означает, что мне нужно запросить всю DOM для этих элементов, что не совсем хорошо просто преобразовывать даты (как насчет отключенного JS?).
Еще
Обычно да, предполагается, что это будет в местном часовом поясе. Но они сделали все возможное, чтобы каждый раз, когда они собирали время от пользователя, оно преобразовывалось в UTC с использованием javascript перед отправкой на сервер. Их решение было очень тяжелым для JS и не очень хорошо деградировало, когда JS был отключен. Я не думал об этом достаточно, чтобы увидеть, есть ли в этом какой-то умник. Но, как я уже сказал, возможно, это даст вам некоторые идеи.
Дж. Холмс
2
С тех пор я работал над другим проектом, который нуждался в местных датах, и я использовал этот метод. Я использую moment.js для форматирования даты / времени ... это мило. Благодарность!
TheCloudlessSky
Разве нет простого способа определить в app.config / web.cofig приложения часовой пояс области приложения и заставить его преобразовывать все значения DateTime.Now в DateTime.UtcNow? (Я хочу избежать ситуаций, когда программисты в компании по-прежнему будут использовать DateTime.Now по ошибке)
Ури Абрамсон
3
Один недостаток этого подхода, который я вижу, заключается в том, что когда вы используете время для других вещей, создания PDF-файлов, электронных писем и т. Д., Для них нет элемента времени, поэтому вам все равно придется преобразовывать их вручную. В остальном очень аккуратное решение
shenku 04
16

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

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)

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

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

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

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

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

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

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

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

На момент написания этой статьи он в настоящее время анализирует все файлы в последнем распределении данных (я фактически запускал его с файлом 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 в местное время (вам придется немного поработать, чтобы преобразовать его в a DateTimeOffset, но это достаточно просто сделать).

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

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

casperOne
источник
Спасибо за подробный ответ. Надеюсь, я не показался мне грубым, но это не помогло мне ответить ни на одну из моих проблем с ASP.NET MVC: как насчет пользовательского ввода (достаточно ли использования настраиваемого связывателя модели)? И самое главное, как насчет отображения этих дат (в часовом поясе пользователя)? Я чувствую, что метод расширения придаст моим взглядам большой вес, который кажется ненужным.
TheCloudlessSky
1
@TheCloudlessSky: см. Последние два абзаца моего отредактированного ответа. Лично я считаю, что детали того, где делать преобразования, несущественны; основная проблема заключается в фактическом преобразовании и хранении данных datetime (кстати, я не могу особо выделить использование здесь datetimeoffsetв SQL Server и DateTimeOffsetв .NET, они действительно значительно упрощают вещи), которые, как я считаю, .NET не обрабатывает должным образом вообще по причинам, изложенным выше. Если у вас есть дата в Нью-Йорке, которая была введена в 2003 году, а затем вы хотите, чтобы она была переведена на дату в Лос-Анджелесе в 2011 году, .NET в этом случае не справится.
casperOne
Проблема с неиспользованием связывателя модели (или любого уровня до действия) заключается в том, что каждый контроллер становится очень трудно тестировать при работе с датами (все они будут зависеть от преобразования даты). Это позволит записывать даты модульных тестов в формате UTC. В моем приложении есть пользователи, связанные с профилем (например, врачи / секретари, связанные с их практикой). Профиль содержит информацию о часовом поясе. Следовательно, довольно легко получить часовой пояс профиля текущего пользователя и преобразовать его во время привязки. У вас есть другие аргументы против этого? Спасибо за ваш вклад!
TheCloudlessSky
Аргументом против этого является то, что ваши тесты не будут точно отражать тестовые примеры. Ваш ввод не будет в формате UTC, поэтому ваши тестовые примеры не должны быть созданы для его использования. Он должен использовать реальные даты с местоположениями и всем остальным (хотя, если вы используете, DateTimeOffsetвы значительно смягчите это, ИМО).
casperOne
Да, но это означает, что каждый тест, связанный с датами, должен учитывать это. Каждое действие, связанное с датой , всегда будет преобразовано в UTC. Для меня это главный кандидат на подшивку модели, а затем проверить подшивку модели .
TheCloudlessSky
5

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

Массимо Зербини
источник
Спасибо за ваш ответ. Да, это то, что я в основном описал, мне просто интересно, как этого можно элегантно достичь с помощью MVC. Приложение сохраняет часовой пояс пользователя как часть создания его учетной записи (они выбирают свой часовой пояс). Проблема снова возникает в том, как отображать даты в каждом представлении без (надеюсь) необходимости засорять их методами, для которых я создал DateTimeExtensions.
TheCloudlessSky
Есть много способов сделать это: один - использовать вспомогательные методы, как вы предложили, другой, возможно, более сложный, но элегантный - это использование фильтра, который обрабатывает запросы и ответы и конвертирует дату и время. Другой способ - разработать настраиваемый атрибут Display для аннотирования полей типа DateTime вашей модели представления.
Массимо Зербини
Это то, что я искал. Если вы посмотрите на мой OP, я также упомянул об использовании AutoMapper для выполнения некоторой обработки перед переходом к результату view / json, но после выполнения действия.
TheCloudlessSky
1

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

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

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

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

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

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

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

dnatoli
источник
Я думаю, что настраиваемые шаблоны отображения / редактора и связыватель - это наиболее элегантное решение, потому что оно будет «просто работать» после реализации. Ни один разработчик не нужно знать ничего особенного , чтобы получить даты рабочих право использовать только DisplayForи , EditorForи он будет работать каждый раз. +1
Кевин Стрикер
17
не будет ли это отображать время сервера приложений, а не время браузера?
Alex
0

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

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

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

Джеффри
источник
0

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

Итак, местный часовой пояс внутри, местный часовой пояс вне. Независимо от того, кто / где / когда пользователь просматривает данные, для наблюдателя это будет местное время, а изменения сохраняются как UTC + локальное смещение.

Вот как я этого добился.

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

// Sets a session variable for local time offset from UTC
function SetTimeZone() {
    var now = new Date();
    var offset = now.getTimezoneOffset() / 60;
    var sign = offset > 0 ? "-" : "+";
    var offset = "0" + offset;
    offset = sign + offset + ":00";
    $.ajax({
        type: "post",
        url: prefixWithSitePathRoot("/Home/SetTimeZone"),
        data: { OffSet: offset },
        datatype: "json",
        traditional: true,
        success: function (data) {
            var data = data;
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert("SetTimeZone failed");
        }
    });
}

Формат предназначен для соответствия формату типа DateTimeOffset SQL Server.

SetTimeZone - просто устанавливает значение переменной Session. Когда пользователь входит в систему, я включаю это значение в кэш профиля пользователя.

2. Когда пользователь отправляет изменение в базу данных, я фильтрую значение DateTime с помощью служебного класса:

cmdADO.Parameters.AddWithValue("@AwardDate", (object)Utility.ConvertLocal2UTC(theContract.AwardDate, theContract.TimeOffset) ?? DBNull.Value);

Метод:

public static DateTimeOffset? ConvertLocal2UTC(DateTime? theDateTime, string TimeZoneOffset)
{
    DateTimeOffset? DtOffset = null;
    if (null != theDateTime)
    {
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        DateTime datetimeUTC = datetime.ToUniversalTime();

        DtOffset = new DateTimeOffset(datetimeUTC.Ticks, AmountOfTime);
    }
    return DtOffset;
}

3. Когда я читаю дату с SQL Server, я делаю следующее:

theContract.AwardDate = theRow.IsNull("AwardDate") ? new Nullable<DateTime>() : DateTimeOffset.Parse(Convert.ToString(theRow["AwardDate"])).DateTime;

В контроллере я изменяю datetime, чтобы оно соответствовало местному времени наблюдателя. (Я уверен, что кто-то может сделать лучше с расширением или чем-то еще):

theContract.AwardDate = Utilities.ConvertUTC2Local(theContract.AwardDate, CachedCurrentUser.TimeZoneOffset);

Метод:

public static DateTime? ConvertUTC2Local(DateTime? theDateTime, string TimeZoneOffset)
{
    if (null != theDateTime)
    {
        TimeSpan AmountOfTime;
        TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime);
        DateTime datetime = Convert.ToDateTime(theDateTime);
        datetime = datetime.Add(AmountOfTime);
        theDateTime = new DateTime(datetime.Ticks, DateTimeKind.Utc);
    }
    return theDateTime;
}

В представлении я просто отображаю / редактирую / проверяю DateTime.

Надеюсь, это поможет кому-то, у кого есть подобная потребность.

Спенсер Салливан
источник