Як елегантно поводитися з часовими поясами


140

У мене веб-сайт, розміщений в іншому часовому поясі, ніж користувачі, які використовують додаток. Крім цього, користувачі можуть мати певний часовий пояс. Мені було цікаво, як до цього підходять інші користувачі та програми 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);
    }
}

Я думаю, що спілкування з часовими поясами було б такою поширеною справою, враховуючи, що багато застосунків зараз є хмарними, де місцевий час сервера може бути набагато іншим, ніж очікуваний часовий пояс.

Це раніше було елегантно вирішено? Чи є щось, чого мені не вистачає? Ідеї ​​та думки дуже цінуються.

EDIT: Щоб очистити деяку плутанину, я думав додати ще детальну інформацію. Проблема зараз полягає не в тому, як зберігати UTC у db, а більше про процес переходу від UTC-> Local та Local-> UTC. Як вказує @Max Zerbini, очевидно розумним є розміщення коду UTC-> Локального коду, але використовуєш DateTimeExtensionsсправді відповідь? Отримуючи дані від користувача, чи має сенс приймати дати як місцевий час користувача (оскільки саме це використовував JS), а потім використовувати a ModelBinderдля перетворення на UTC? Часовий пояс користувача зберігається в БД і легко отримується.


3
Ви можете спочатку прочитати цей чудовий пост ... Перехід на літній час та найкращі практики часового поясу
dodgy_coder

@dodgy_coder - Це завжди було чудовою ланкою ресурсів для часових поясів. Однак це насправді не вирішує жодної з моїх проблем (зокрема, стосується MVC). Спасибі, хоча.
TheCloudlessSky

code.google.com/p/noda-time може бути корисним
Arnis Lapsa

Цікаво, яке рішення ви обрали. Я сам стикаюся з подібним рішенням. Гарне питання.
Шон

@Sean - рішення досі не було елегантним (саме тому я ще не прийняв відповідь). Було багато ручних накладних витрат на друк дат / часів і вручну перетворення їх назад на ModelBinder.
TheCloudlessSky

Відповіді:


106

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

  • Усі дати на сервері - UTC. Це означає , використовуючи, як ви сказали, DateTime.UtcNow.

  • Постарайтеся якомога менше довіряти клієнту дати передачі сервера. Наприклад, якщо вам потрібно "зараз", не створюйте дату на клієнті і не передайте її серверу. Або створіть дату в GET і передайте її в ViewModel або на POST do 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властивість даних для форматування дати та заповнення вмісту елемента.

Таким чином, їм ніколи не доводилося стежити, зберігати або керувати часовим поясом клієнтів. Сервер не хвилювався, в якому часовому поясі знаходився клієнт, і не довелося робити жодного перекладу часового поясу. Він просто виплюнув UTC і дозволив клієнту перетворити це на щось розумне. Це легко в браузері, оскільки він знає, в якому часовому поясі знаходиться. Якщо клієнт змінив свій часовий пояс, веб-додаток автоматично оновлюється. Єдине, що вони зберігали - це рядок формату дати для місцевості користувача.

Я не кажу, що це був найкращий підхід, але це був інший, якого я раніше не бачив. Можливо, ви з цього почнете цікаві ідеї.


Дякую за Вашу відповідь. При отриманні введення дати від користувача (наприклад , призначення про плануванні), що не цей вхід передбачається , що в їх поточному часовому поясі? Що ви думаєте про те, що ModelBinder робить цю трансформацію перед тим, як вступити в дію (дивіться мої коментарі до @casperOne. Також <time>ідея досить крута. Єдиний недолік - це те, що це означає, що мені потрібно запитати цілий DOM на ці елементи, які не зовсім приємно просто перетворити дати (що з JS вимкнено?) Ще раз дякую!
TheCloudlessSky

Зазвичай так, припущення полягає в тому, що це було б в локальному часовому поясі. Але вони вирішили переконатися, що щоразу, коли вони збирають час від користувача, він буде перетворений на UTC за допомогою javascript перед тим, як його відправити на сервер. Їх рішення було дуже важким JS, і не дуже добре погіршилося, коли JS вимкнувся. Я не задумався над цим достатньо, щоб побачити, чи є розумний навколо цього. Але, як я сказав, можливо, це дасть вам кілька ідей.
Дж. Холмс

2
З тих пір я працював над іншим проектом, який потребував місцевих дат, і це був метод, який я використовував. Я використовую moment.js для форматування дати / часу ... це солодко. Дякую!
TheCloudlessSky

Чи не існує простого способу визначити в app.config / web.cofig програми часовий пояс програми та змусити перетворити всі значення DateTime.Now у DateTime.UtcNow? (Я хочу уникати ситуацій, коли програмісти в компанії все-таки використовуватимуть DateTime.Серед помилково)
Урі Абрамсон

3
Один недолік цього підходу, який я бачу, - це коли ви використовуєте час для інших речей, створюючи PDF-файли, електронні листи та інше, для них немає елемента часу, тому вам доведеться конвертувати їх вручну. Інакше дуже акуратне рішення
shenku

15

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

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

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


Це здається працездатним, якщо хтось не має n-ярусної архітектури або спільні бібліотеки. Як ми можемо поділити ідентифікатор часового поясу на рівень даних.
Вікаш Кумар

9

У розділі подій на sf4answers користувачі вводять адресу події, а також дату початку та необов'язкову дату закінчення. Ці часи переводяться на datetimeoffsetсервер SQL, який враховує зміщення від UTC.

Це та сама проблема, з якою ви стикаєтесь (хоча ви до цього використовуєте інший підхід, який ви використовуєте DateTime.UtcNow); у вас є місцеположення, і вам потрібно перекласти час з одного часового поясу в інший.

Є дві основні речі, які я зробив, які працювали на мене. По-перше , завжди використовуйте DateTimeOffsetструктуру . Він припадає на компенсацію від UTC, і якщо ви можете отримати цю інформацію від свого клієнта, це полегшить ваше життя.

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

Маючи базу даних в руці, ви можете використовувати API 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 та веб-сайтах, це залежить від аудиторії та того, що ви намагаєтеся показати. Якщо ви помітили, більшість соціальних веб-сайтів (і так, і розділ подій на sf4answers) відображають події відносно час, або, якщо використовується абсолютне значення, зазвичай це UTC.

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

Де зробити трансформацію? Це вартість, яку вам доведеться десь заплатити , і немає "найкращого" способу. Хоча я б обрав перегляд із зміщенням часового поясу як частини моделі подання, представленої на вид. Таким чином, якщо вимоги до перегляду змінюються, вам не доведеться змінювати модель перегляду, щоб прийняти зміни. Ви JsonResultпросто містили б модель із зміщенням та зсувом.IEnumerable<T>

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


Дякуємо за детальну відповідь. Я сподіваюся, що я не сприймаю себе як грубий, але це насправді не відповіло на жодну з моїх проблем з ASP.NET MVC: А що з введенням користувача (чи достатньо буде використання власної палітурки моделі)? І найголовніше, що з відображенням цих дат (у місцевому часовому поясі користувача)? Я відчуваю, що метод розширення додасть великої ваги моїм поглядам, що здається непотрібним.
TheCloudlessSky

1
@TheCloudlessSky: Дивіться останні два абзаци моєї відредагованої відповіді. Особисто я вважаю, що деталі, де робити трансформації, є незначними; головне питання - власне перетворення та зберігання даних дати (btw, я не можу наголосити достатньо на використанні datetimeoffsetв SQL сервері та DateTimeOffsetв .NET тут, вони дійсно надзвичайно спрощують речі), на який я вважаю .NET не справляється належним чином. взагалі з наведених вище причин. Якщо у вас є дата в Нью-Йорку, яка була введена в 2003 році, і ви хочете, щоб вона була переведена на дату в Лос-Анджелесі в 2011 році.
casperOne

Проблема з тим, що не використовується модельне в'яжуче (або будь-який шар до дії), полягає в тому кожен контролер стає дуже важким для перевірки під час роботи з датами (всі вони отримають залежність від перетворення дати). Це дозволить записувати одиничні дати випробувань у UTC. У моїй програмі є користувачі, пов’язані з профілем (думаю, лікарі / секретарі, пов’язані з їх практикою). Профіль містить інформацію про часовий пояс. Тому досить легко отримати часовий пояс профілю поточного користувача та перетворити його під час прив’язки. Чи є у вас інші аргументи проти цього? Дякуємо за ваш внесок!
TheCloudlessSky

Справа проти цього полягає в тому, що ваші тести не будуть точно відображати тестові випадки. Ваш внесок є не буде в UTC, тому не слід створювати тестові випадки, щоб використовувати його. Він повинен використовувати дати в реальному світі з місцями розташування та всіма (хоча якщо ви користуєтесь цим, DateTimeOffsetви значно пом'якшите це, IMO).
casperOne

Так - але це означає, що кожен тест, який стосується дат, повинен враховувати це. Кожна дія, яка стосується дат буде завжди перетворена на UTC. Для мене це основний кандидат на в'яжучу модель, а потім перевірити в'яжучу модель .
TheCloudlessSky

5

Це лише моя думка, я вважаю, що програма MVC повинна відокремити проблему представлення даних добре від управління моделлю даних. База даних може зберігати дані в локальний час сервера, але обов'язок шару презентації рендерувати дату, використовуючи місцевий часовий пояс користувача. Це здається мені тією самою проблемою, що і I18N та формат чисел для різних країн. У вашому випадку ваша програма повинна виявити Cultureі часовий пояс користувача та змінити Перегляд із відображенням різного тексту, кількості та дати подання, але збережені дані можуть мати однаковий формат.


Дякую за Вашу відповідь. Так, це я в основному описав, мені просто цікаво, як це можна елегантно досягти за допомогою MVC. Додаток зберігає часовий пояс користувача як частину створення облікового запису (вони вибирають часовий пояс). Проблема знову виникає в тому, як відображати дати в кожному огляді, не маючи (сподіваємось), що їх потрібно засмічувати методами, які я створив DateTimeExtensions.
TheCloudlessSky

Є багато способів зробити це, один - використовувати допоміжні методи, як ви запропонували, інший, можливо, більш складний, але елегантний - це використання фільтра, який обробляє запити та відповіді та перетворює дату. Інший спосіб - розробити спеціальний атрибут Display для анотації полів типу DateTime вашої моделі перегляду.
Массімо Зербіні

Це такі речі, які я шукав. Якщо ви подивитесь на мій ОП, я також згадав про використання AutoMapper, щоб зробити деяку обробку, перш ніж перейти до результату view / json, але після того, як дія буде виконана.
TheCloudlessSky

1

Для виводу створіть такий шаблон відображення / редактора

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

Ви можете зв'язати їх на основі атрибутів вашої моделі, якщо ви хочете, щоб ті шаблони використовували лише певні моделі.

Детальніше про створення спеціальних шаблонів редактора див. Тут і тут .

Крім того, оскільки ви хочете, щоб він працював і для введення, і для виводу, я б запропонував розширити контроль або навіть створити свій власний. Таким чином ви можете перехоплювати і вхід, і вихід, і перетворювати текст / значення за потребою.

Сподіваємось, це посилання спонукає вас у правильному напрямку, якщо ви хочете піти цим шляхом.

У будь-якому випадку, якщо ви хочете елегантне рішення, його буде трохи працювати. З іншого боку, як тільки ви це зробите, ви зможете зберегти його у своїй бібліотеці кодів для подальшого використання!


Я думаю, що власні шаблони дисплея / редактора та палітурки є найелегантнішим рішенням, оскільки воно "просто спрацює" після його впровадження. Ні один розробник не потрібно знати нічого особливого , щоб отримати дати робочих право використовувати тільки DisplayForі , EditorForі він буде працювати кожен раз. +1
Кевін Стрікер

17
Це не відображатиме час сервера додатків, а не час браузера?
Олексій

0

Це, ймовірно, кувалда для зламу гайки, але ви можете ввести шар між шарами інтерфейсу користувача та Business, який прозоро перетворює дати часу в місцевий час на повернених графіках об'єкта та в UTC на вхідні параметри дати часу.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.