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

TL;DR


  • Необходимо различать термины:
    • UTC — локальное время в зоне +00:00, без эффекта DST
    • DateTimeOffset — локальное время со смещением от UTC ±NN:NN, где смещением является базовое смещение от UTC без эффекта DST (в C# TimeZoneInfo.BaseUtcOffset)
    • DateTime — локальное время без информации о таймзоне (мы игнорируем признак Kind)
  • Разделение использования на внешнее и внутренее:
    • Входящие и исходящие данные через API, сообщения, файловые экспорты/импорты должны быть строго в UTC (тип DateTime)
    • Внутри системы данные храняться вместе со смещением (тип DateTimeOffset)
  • Разделение использования в старом коде на не-БД код (C#, JS) и БД:
    • не-БД код оперирует только с локальными значениями (тип DateTime)
    • БД работает с локальными значениями + смещение (тип DateTimeOffset)
  • Новые проекты (компоненты) используют DateTimeOffset.
  • В БД тип DateTime просто меняется на DateTimeOffset:
    • в типах полей таблиц
    • в параметрах хранимок
    • в коде фиксятся несовместимые конструкции
    • к пришедшему значению присоединяется информация о смещении (простая конкатенация)
    • перед отдачей в не-БД код значение приводится к локальному
  • Никаких изменений в не-БД коде
  • DST решается использованием CLR Stored Procedures (для SQL Server 2016 можно использовать AT TIME ZONE).


Теперь детальнее о преодоленных сложностях.

«Вшитые» стандартны IT индустрии


Потребовалось довольно много времени, чтобы избавить людей от страха хранить даты в локальном времени со смещением. Некоторое время назад, если спросить программиста с опытом: «Как поддержать таймзоны?» — единственным вариантом был: «Используй UTC, а конвертируй в локальное время только перед показом». Тот факт, что для нормальной работы все равно необходима дополнительная информация, такая, как смещение и названия таймзон, был спрятан под капотом реализации. С появлением DateTimeOffset такие детали вылезли наружу, но инертность «программистского опыта» не позволяет быстро согласиться с другим фактом: «Хранение локальной даты с базовым смещением от UTC» — это тоже самое, что хранение UTC. Еще один плюс использования DateTimeOffset повсеместно позволяет делегировать контроль за соблюдением таймзон .NET Framework и SQL Server, оставив для человеческого контроля только моменты ввода и вывода данных из системы. Под человеческим контролем я имею ввиду написанный программистом код для работы с date/time значениями.

Чтобы преодолеть подобный страх пришлось провести не одну сессию с разъяснениями, представляя примеры и Proof Of Concept. Чем проще и ближе примеры к тем задачам, которые решаются в проекте, тем лучше. Если пускаться в рассуждения «вообще», то это приводит к усложнению понимания и трате времени впустую. Коротко: меньше теории — больше практики. Аргументы за UTC и против DateTimeOffset можно отнести к двум категориям:

  • «UTC all the time» является стандартом и остальное не работет
  • UTC решает проблему с DST

Следует отметить, что ни UTC, ни DateTimeOffset не решают проблему с DST без использования информации о правилах конвертации между зонами, которая доступна через класс TimeZoneInfo в C#.

Упрощенная Модель


Как выше отметил, в старом коде изменения происходят только в БД. Как именно это работает можно оценить на простом примере.

Пример модели в T-SQL
-- 1) сохранение данных
-- входящие данные в локали пользователя, как он их видит
declare @input_user1 datetime = '2017-10-27 10:00:00'
-- в конфигурации пользователя есть информация о зоне
declare @timezoneOffset_user1 varchar(10) = '+03:00'
 
declare @storedValue datetimeoffset
-- при получении значений присоединяем смещение пользователя
set @storedValue = TODATETIMEOFFSET(@input_user1, @timezoneOffset_user1)
-- это значение будет сохранено
select @storedValue 'stored'
 
-- 2) отображение информации
-- в конфигурации 2го пользователя указана другая таймзона
declare @timezoneOffset_user2 varchar(10) = '-05:00'
-- перед выдачей в клиентский код значения приводятся к локальным
-- так будут выглядеть данные в базе и на дисплеях пользователей
select
@storedValue 'stored value',
CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow',
CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY'
 
-- 3) теперь 2й пользователь сохраняет данные
declare @input_user2 datetime
-- на вход приходят локальные значения, как он их видит в Нью-Йорке
set @input_user2 = '2017-10-27 02:00:00.000'
 -- соединяем с информацией о смещении
set @storedValue = TODATETIMEOFFSET(@input_user2, @timezoneOffset_user2)
select @storedValue 'stored'
 
-- 4) отображение информации
select
@storedValue 'stored value',
CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user1)) 'user1 Moscow',
CONVERT(DATETIME, SWITCHOFFSET(@storedValue, @timezoneOffset_user2)) 'user2 NY'


Результат выполнения скрипта будет следующим.



По примеру видно, что данная модель позволяет делать изменения только в БД, что значительно уменьшает риск возникновения дефектов.

Примеры функций для обработки date/time значений
-- При получении значений из не-БД кода в DateTimeOffset они будут локальными, но со смещением +00:00, поэтому необходимо присоединить смещение юзера, но конвертировать между поясами нельзя. Для этого переведем значение в DateTime и потом обратно уже с указанием смещения
-- DateTime без проблем конвертируется в DateTimeOffset, поэтому изменять вызов хранимок в клиентском коде не надо

create function fn_ConcatinateWithTimeOffset(@dto datetimeoffset, @userId int)
returns DateTimeOffset as begin
    declare @user_time_zone varchar(10)
    set @user_time_zone = '-05:00' -- из настроек юзера @userId
    return todatetimeoffset(convert(datetime, @dto), @user_time_zone)
end

-- Клиентский код не может читать DateTimeOffset в переменные типа DateTime, поэтому необходимо не только сконвертировать в в нужную таймзону, но и привести к DateTime, иначе произойдет ошибка 

create function fn_GetUserDateTime(@dto datetimeoffset, @userId int)
returns DateTime as begin
    declare @user_time_zone varchar(10)
    set @user_time_zone = '-05:00' -- из настроек юзера @userId
    return convert(datetime, switchoffset(@dto, @user_time_zone))
end


Маленькие Артифакты


В ходе адаптации SQL кода были обнаружены некоторые вещи, которые работают для DateTime, но несовместимы с DateTimeOffset:

  • GETDATE()+1 надо заменить на DATEADD(day, 1, SYSDATETIMEOFFSET())
  • ключевое слово DEFAULT несовместимо с DateTimeOffset, надо использовать SYSDATETIMEOFFSET()
  • конструкция ISNULL(date_field, NULL) > 0" работает с DateTime, но для DateTimeOffset должна быть заменена «date_field IS NOT NULL»

Заключение или UTC vs DateTimeOffset


Кто-то может заметить, что как и в подходе с UTC мы занимаемся конвертацией при получении и при отдаче данных. Тогда зачем это все, если есть проверенное и работающее решение? Есть несколько причин этому:

  • DateTimeOffset позволяет забыть где находится SQL Server.
  • Это позволяет переложить часть работы на систему.
  • Конвертации можно свести к минимуму, если DateTimeOffset используется везде, делая их только перед отображением данных или выдачи их во внешние системы.

Эти причины нам показались существенными за использование описанного подхода. Буду рад ответить на вопросы, пишите в коментах.
Поделиться с друзьями
-->

Комментарии (62)


  1. qw1
    02.04.2017 09:46

    «Хранение локальной даты с базовым смещением от UTC» — это тоже самое, что хранение UTC
    Нет, это больше, чем просто UTC. В России часто происходит дурдом с изменением зон в разных городах, типа давайте с нового года жить в UTC+03 вместо UTC+04, или давайте отменим летнее время, потом снова его введём, потом снова отменим.

    Поэтому, например, для программы кадрового учёта, которая хранит время сотрудников на работу, UTC — лишние проблемы (если посмотреть на 2 года в прошлое, 5:00/UTC — это опоздание на час или приход вовремя?)

    Конвертации можно свести к минимуму, если DateTimeOffset используется везде, делая их только перед отображением данных
    Тут всё равно нужен анализ ситуаций. Иногда время 15:00 MSK показывать в Нью-Йорке как есть (с припиской «по Москве»), иногда конвертить в местное.


    1. ggrnd0
      02.04.2017 10:22

      Кроме того:


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

      Ничего не изменилось, только вместо 2х полей в БД используется одно.


      1. ETman
        04.04.2017 08:21
        -1

        Так и есть. Просто адепты UTC пропускают этот факт: все равно надо будет обеспечить хранение смещения. Толька в случае с UTC all the time самостоятельно с вытекающими из этого «напрягами».


        1. ggrnd0
          04.04.2017 10:29

          Тут вопрос в другом:
          — имеет ли смысл переписывать существующие приложения?
          — Нет
          — имеет ли смысл переписывать библиотеки/платформу, которые уже используются в 50+ проектах?
          — Опять нет.

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


          1. ETman
            04.04.2017 13:50
            +1

            Имеет смысл или нет зависит от проекта. Если у вас не было поддержки таймзон, то переписывать в любом случае придется. И если сравнивать два подхода с UTC-all-the-time и DateTimeOffset, то при равных затратах на переписывание, второй проще технически.


  1. andreylartsev
    02.04.2017 10:36
    +1

    Таймзона это безусловно дополнительная информация ко времени события. Но описанный случай со временем прихода на работу является практически исключительным, и в этом случае я бы даже хранил таймзону как отдельное поле ) чтобы всем было понятно что эта информация которую нельзя потерять при всяких конвертациях ) Но если вам необходимо просто абсолютное время произошедшего события то таймзона лишняя информация которой просто не должно быть на уровне БД.
    Используя принцип Оккама не умножать сущности без необходимости )
    Вывод времени события в таймзоне пользователя относится к презентационной логике и конвертация в локальное время пользователя по моему скромному времени должна быть только там а не в хранимых процедурах и не в каком либо t-sql.

    То же самое относится к загрузкам данных, у разных источников данных могут быть разные соглашения о формате времени и таймзоне, но задачу конвертации в один общий формат и одну таймзону БД лучше решать именно там в каждом конкретном сервисе загрузки данных.
    Кстати таймзона БД не обязательно должна быть UTC, иногда удобнее хранить данные в таймзоне локальной для основной команды разработки. Но главное чтобы к этой таймзоне не применялись правила DST. То есть если EST так уж EST никаких там ESD.


  1. DexterHD
    02.04.2017 12:49
    +3

    Что то мне подсказывает то все эти проблемы и изыскания от непонимания того, что такое «UTC» и что такое «Часовой пояс» (Timezone). А самое главное, как правильно это использовать.

    единственным вариантом был: «Используй UTC, а конвертируй в локальное время только перед показом»

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

    Для нормальной работы клиент(т.е. пользователь) должен так или иначе сообщить системе, в каком часовом поясе и в какой стране он находится. Иначе любые танцы с бубном не имеют ни какого смысла.

    Чтобы не было никаких проблем достаточно 3 вещи:
    1. Время везде хранись в UTC
    2. Знать список правил, как устанавливать смещение относительно UTC (В зависимости от часового пояса, времени года, страны)
    3. Знать Часовой пояс пользователя, а так же страну его местонахождения.

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


    1. qw1
      02.04.2017 13:15

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


      1. DexterHD
        02.04.2017 13:35
        +1

        Этот конкретный юзкейс решается хранением данных о том, где и в каком часовом поясе это событие произошло.
        Причем для любого кол-ва событий произошедших географически в одном месте достаточно хранить всего 1 значение с информацией о том, где и в каком часовом поясе событие произошло.

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


        1. qw1
          02.04.2017 19:26
          -1

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


        1. allter
          03.04.2017 00:42
          -1

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

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

          А по топику — ваш подход к UTC, как «единственному правильному» способу хранения дат — пример упрощённого и шапкозакидательского подхода к работам с датами. Благодаря таким же чувакам мне сейчас приходится на смартфоне использовать временную зону Кувейта, из-за долговременной стабильности оной (и невозможности часто просто указать фиксированную временную зону как смещение). То тут, то там оказывается, что кто-то использовал то или иное умолчание, которое, почему-то, оказалось некорректным.

          По факту, термины: 1) время в UTC, 2) время в локальной временной зоне (политически установленной), 3) время в временной зоне ±IJKL, где IJKL — цифры, 4) время в т.н. «floating» временной зоне — описывают разные явления нашей жизни. И подход «используй везде UTC», как минимум, вследствие этого факта, неверен.

          P.S. Также неверно путать системное время в компьютере (в т.ч. в СУБД и в других серверах) и время как физическую абстракцию/модель. В частности, в компьютере время не монотонно возрастающее. :)


          1. Varim
            03.04.2017 01:05

            В частности, в компьютере время не монотонно возрастающее. :)
            Я подзабыл значение монотонный, вы имеете в виду что в компьютере дискретно?


            1. allter
              03.04.2017 01:12

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


              1. Varim
                03.04.2017 03:44

                Я удивлен. Запросто можно получить одно и то же время на том же компьютере, но кажется я не встречал что бы таймер шел назад. Связан ли обратный отсчет с несколькими процессорами? Может связан с NUMA…


                1. mayorovp
                  03.04.2017 08:05

                  Он связан с возможностью крутить системные часы в любую сторону. Этим занимается либо пользователь — либо служба синхронизации времени (на линуксе — ntpd). А еще иногда на материнке дохнет батарейка.


          1. DexterHD
            03.04.2017 09:48

            Благодаря таким же чувакам мне сейчас приходится на смартфоне использовать временную зону Кувейта, из-за долговременной стабильности оной

            А для меня такие чуваки, на моем смартфоне запилили галочку «Устанавливать время автоматически»
            И вот уже 10 лет как со временем у меня нет проблем.
            А по топику — ваш подход к UTC, как «единственному правильному» способу хранения дат — пример упрощённого и шапкозакидательского подхода к работам с датами

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


            1. Ogra
              03.04.2017 12:59

              А для меня такие чуваки, на моем смартфоне запилили галочку «Устанавливать время автоматически»
              И вот уже 10 лет как со временем у меня нет проблем.

              Попробовал поставить на своем смартфоне такую галочку. Время резко изменилось на час. Выключил. Поверьте, автоматика не всегда работает, или забыли про историю со сменой часовых поясов в России, как на многих смартфонах будильники не сработали?

              Этот поход придумал не я, его годами выверяли и стандартизировали. Но в вашем мировоззрении конечно все дурачки, напридумывали всякой фигни

              Понимаете, в чем дело — у подобных стандартов есть область применения, и не всегда стоит их слепо применять. Всегда нужно думать о своей задаче. Например, для задачи «контролировать опоздания работников» локальное время важно, а UTC и таймзоны нет. Решать задачу через несвязанные с ней абстрактные (хоть и стандартные) инструменты — несколько неправильно, не находите?


  1. wildraid
    02.04.2017 13:26
    +2

    Главная проблема datatype'ов с поддержкой таймзон — отсутствие единого железного стандарта о том, как они должны работать. Как видим, даже внутри SQL Server поведение типа DateTimeOffset немного отличается от DateTime.

    Что будет, если понадобится выгрузить эти данные для обработки в Hive, в BigQuery, в системы аналитики, в визуализаторы (Tableau etc.)? Что будет, если захотим поменять СУБД целиком?


  1. justdev
    02.04.2017 19:50
    +1

    Спасибо за статью. Но все на самом деле еще сложнее и интереснее (немного больше подробностей тут Подводные камни Date & Time — Илья Фофанов).


  1. corr256
    02.04.2017 22:38

    Где это вы использовали GETDATE()+1 и ISNULL(date_field, NULL) > 0? oO


    1. ETman
      03.04.2017 02:04

      Это артифакты доставшиеся из далекого прошлого. Было любопытно их обнаружить.


  1. flancer
    03.04.2017 11:29

    Полностью согласен с коллегой DexterHD. Если все данные в системе хранятся и обрабатываются в UTC, то остается всего одна проблема — определить желаемую временнУю зону при представлении этих данных конечному пользователю. Другие варианты не обладают универсальностью и хороши лишь в определенных условиях и до тех пор, пока эти условия не изменятся.


    1. ETman
      04.04.2017 08:23
      -2

      Оба варианта решают задачу. DateTimeOffset делает решение проще, на мой взгляд. Описал тут в ответе. Что думаете?


  1. bluetooth
    03.04.2017 12:01
    +1

    Прямо «неделя проповедничества DateTimeOffset» на Хабре…


  1. Pilat
    03.04.2017 12:15

    На Хабре вопрос временных зон обсуждается регулярно и давно уже известно, что надо хранить время так, чтобы метод хранения решал поставленные задачи. Например, в какой зоне хранить дату "через месяц в три часа дня"? В текущей? В будущей? В той, которая будет принята правительством через две недели?


    Для абстрактного проекта действительно даты надо хранить либо в UTC, либо в формате с указанием смещения от UTC. Это позволит в крайнем случае скачать базу данных временных зон и получить местное время — почти всегда, так как местное время иногда устанавливается волевым решением руководства лодочной станции. Это позволяет синхронизировать события. Это устраняет некоторые странные проблемы. Но пример выше показывает, что в некоторых случаях это недостаточно. Просто надо помнить что время — самая сложная концепция в программировании (после выбора названия для объектов).


  1. w1ld
    03.04.2017 12:21
    +2

    Действительно инертность есть. Не хочется перелопачивать код в DateTimeOffset. Просмотрел статью, не убедила в использовании этого другого подхода. Может быть кто-нибудь кратко объяснит, в чем преимущество этого способа перед «везде внутри используем UTC»?


    1. ETman
      04.04.2017 08:17

      Например, следующие вещи меня убедили в том, что DateTimeOffset лучше чем UTC all the time.

      Меньше волнений из-за:

      • меньше волнений из-за сервера БД, т.к. он может работать в любой зоне (если кто-то случайно поменял ее — пофиг)
      • меньше кода проверок таймзоны входящих значений (например, например, проверка всяких Kind и действительно ли пришедшая дата в зоне N, а не UTC?)
      • проще соглашения по использованию дат внутри системы (не надо постоянно заботиться о передаче UTC)
      • проще обработка данных из разных таймзон, т.к. многие рутинные вещи выполняет фреймворк
      • новые C# проекты могут вовсе заботиться о конвертации только в момент, когда выводится пользователю. А сохранять в БД значения со смещением, которое предоставляется система. Т.е. разрабу вообще не надо будет думать о всяких конвертациях и UTC пока он не возвращает данные наружу (экран или внешние системы)

      Простой переезд, т.к. в БД DateTimeOffset кастится к DateTime и обратно без ошибок, только отрезается информация о смещении. Поэтому на время переезда вы спокойно можете сосредоточиться только на базе, не трогая C#. Дат, обычно немного.

      НО:
      При чтении SQL DateTimeOffset в C# DateTime возникает исключение. Затраты на компенсацию соизмеримы с затратами по поддержке UTC. При этом смещение вам, скорее всего, все равно понадобится хранить рано или поздно. Только с подходом UTC вы обрекаете себя на дополнительные муки. Так же не стоит забывать о проблемах, которые связаны с DateTime.


      1. mayorovp
        04.04.2017 08:33

        Простите, но какие проблемы могут в принципе возникнуть при смене временной зоны сервера в подходе "all UTC"? Зачем проверять Kind если известно, что все даты — в UTC?


        1. ETman
          04.04.2017 13:55

          Потому что DateTime приводится к UTC в зависимости от Kind. Если проскочит неверный, то возникнет ошибка, которую вы просто так не отловите. Например, кто-то забыл выставить Kind, или наоборот выставил, когда не надо было. Вручную конвертацию производить? Без четкого обозначения смещения, вы обрекаете девелопера помнить об этом постоянно. А потом еще и тикеты из саппорта разгребать.


        1. ETman
          04.04.2017 14:00
          -1

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


          1. mayorovp
            04.04.2017 14:15

            Да с чего им переезжать-то?!


            1. ETman
              04.04.2017 14:20

              Не понял вопрос. Кому «им»?


              1. mayorovp
                04.04.2017 14:25

                Датам, кому же еще. Вот лежит у меня в базе дата в UTC. Я меняю у сервера тайм-зону. Что, по-вашему, случится с датой?


                1. ETman
                  04.04.2017 15:51
                  -2

                  Давайте рассмотрим случай:
                  — из базы достается ДТ значение без оффсета (DateTime)
                  — в коде C# вы конвертируете это в зону юзера, предполагая, что пришли UTC

                  1) Проблема с чтением из БД

                  var utc = new DateTime(2017, 4, 4, 10, 0, 0); // from DB expecting UTC
                  var targetTz = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
                  var local = TimeZoneInfo.ConvertTime(utc, targetTz);
                  
                  Console.WriteLine("utc: {0}", utc);
                  Console.WriteLine("local: {0}", local);
                  


                  Результат:
                  utc: 04/04/2017 10:00:00
                  local: 04/04/2017 10:00:00


                  Решение: Надо поставить Kind в Utс, или использовать TimeZoneInfo.ConvertTimeFromUtc().

                  2) Проблема с датами при сохранении
                  Суть примера: юзер вводит два раза одну и туже дату, но первую он вводит пока сервер в одной таймзоне, а вторую — когда в другой. Затем сервер пытается привести все к дате юзера и терпит неудачу, т.к. они не равны. А должны быть одинаковые.

                  var server1Timezone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
                  var server2Timezone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
                  
                  var userInput1 = new DateTime(2017, 4, 4, 13, 0, 0);
                  var utc1 = TimeZoneInfo.ConvertTimeToUtc(userInput1, server1Timezone);
                  
                  var userInput2 = new DateTime(2017, 4, 4, 13, 0, 0);
                  var utc2 = TimeZoneInfo.ConvertTimeToUtc(userInput2, server2Timezone);
                  
                  var targetTz = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
                  var local1 = TimeZoneInfo.ConvertTimeFromUtc(utc1, targetTz);
                  var local2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, targetTz);
                  
                  Console.WriteLine("display: {0}", local1);
                  Console.WriteLine("display: {0}", local2);
                  

                  Результат:
                  display1: 04/04/2017 13:00:00
                  display2: 04/04/2017 20:00:00

                  Следует учесть, что такие строчки, как TimeZoneInfo.ConvertTimeToUtc(userInput1, server1Timezone); будут выполняться самой системой, т.е. это придется как-то решать. И если пропустите, то никто не упадет и не скажет об ошибке. Например, в SQL и в C# у вас могут использоваться такие вещи, которые оперируют с локальной таймзоной сервра. Например, GETDATE() или TimeZoneInfo.ConvertTime(). Вам надо будет постоянно следить за правильностью кода.

                  DateTimeOffset убирает «предположения» из работы с датами.


                  1. mayorovp
                    04.04.2017 16:03

                    С какого перепугу у вас дата, введенная пользователем, интерпретируется исходя из тайм-зоны сервера?


                    1. ETman
                      04.04.2017 16:26
                      -1

                      Это же только пример, показывающий природу неоднозначности DateTime.


                      1. mayorovp
                        04.04.2017 16:27

                        В таком случае, вот это — пример, показывающий неоднозначность типа данных int:


                        var x = 42;
                        x = x+1;
                        Console.WriteLine($"{x} = 42");


                        1. ETman
                          04.04.2017 17:07
                          -1

                          Речь о том, что для правильной работы с UTC-all-the-time вам надо постоянно помнить о том, что:
                          — какая зона у юзера
                          — правильно ли я произвел конвертацию
                          — правильно ли передался Kind в момент, когда это важно

                          C DateTimeOffset вам придется об этом думать только в момент перед выдачей результата (на экран или во внешнюю систему).


                          1. mayorovp
                            04.04.2017 17:12

                            Не нужно это все помнить. Подход UTC-all-the-time подразумевает, что конвертация производится ровно 1 раз, на границе подхода. Надо просто сделать ее и забыть.


                            1. ETman
                              04.04.2017 18:13
                              -1

                              Такой оптимизм рождает потом баги.


                              1. mayorovp
                                05.04.2017 10:05
                                -1

                                Баги рождает не оптимизм, а отсутствие соглашений.


                  1. michael_vostrikov
                    04.04.2017 18:25

                    Решение: Надо поставить Kind в Utс, или использовать TimeZoneInfo.ConvertTimeFromUtc().

                    Так и используйте, в чем проблема?

                    первую он вводит пока сервер в одной таймзоне, а вторую — когда в другой

                    Таймзона сервера тут ни при чем. Должно быть примерно так:

                    var userTimezone = getUserTimeZoneFromSomeStorage();
                    
                    var userInput1 = new DateTime([params from user input]);
                    var utc1 = TimeZoneInfo.ConvertTimeToUtc(userInput1, userTimezone);
                    
                    var userInput2 = new DateTime([params from user input]);
                    var utc2 = TimeZoneInfo.ConvertTimeToUtc(userInput2, userTimezone);
                    
                    // save to DB
                    
                    ...
                    
                    // show data from DB
                    
                    var utc1 = new DateTime([params from DB]);
                    var utc2 = new DateTime([params from DB]);
                    
                    var userTimezone = getUserTimeZoneFromSomeStorage();
                    
                    var local1 = TimeZoneInfo.ConvertTimeFromUtc(utc1, userTimezone);
                    var local2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, userTimezone);
                    
                    Console.WriteLine("display: {0}", local1);
                    Console.WriteLine("display: {0}", local2);
                    


                    В БД положили, и оно там лежит. Изменение таймзоны БД на сохраненные данные не влияет. Изменение таймзоны сервера на выполняющийся код не влияет. Таймзона пользователя в пределах запроса одна и та же. Все выборки с фильтрами или вычисления в бизнес-логике можно делать в UTC, конвертирование только при выводе.


                    1. ETman
                      04.04.2017 20:37

                      Никто не спорит, что с UTC это можно. Но зачем руками, когда DateTimeOffset позволяет все тоже самое с меньшими усилиями?


                      1. michael_vostrikov
                        04.04.2017 21:12

                        А какой код будет с DateTimeOffset?

                        А какой код будет с DateTimeOffset, если одни и те же данные десятиминутной давности могут смотреть 2 пользователя в разных таймзонах?


                        1. michael_vostrikov
                          04.04.2017 21:16

                          Я к тому, что в статье у вас вроде то же самое, только на SQL. Зачем использовать связку SQL + C#, если можно использовать просто C#?


                          1. ETman
                            05.04.2017 07:52
                            -1

                            Потому что при использовании C# вы будете делать компенсирующие вещи, такие как конвертация в UTC и обратно, учет Kind и прочее. При «связке» вам ничего ненадо делать, т.к. работаете так, словно зон нет ровно до момента вывода данных. Я обобщаю, конечно, т.к. работать надо будет с DateTimeOffset, что чуть-чуть иначе, чем с DateTime.


                1. qw1
                  04.04.2017 19:26
                  +2

                  Я меняю у сервера тайм-зону. Что, по-вашему, случится с датой?
                  Когда есть куча legacy-кода, в котором есть вызовы DateTime.Now и GETDATE(), причём ещё как-то неявно (например, из сторонних компонент), то смена зоны сервера приложений или сервера БД станет проблемой.

                  Если сразу всё писалось и тестилось с DateTime.UtcNow и GETUTCDATE(), то проблемы не будет.


      1. ggrnd0
        04.04.2017 10:38

        Зачем проверять Kind, если есть `DateTimeStyles`?


        1. ETman
          04.04.2017 13:56

          Выше ответил.


          1. ggrnd0
            04.04.2017 14:18

            DateTimeStyles определяет как интерпретировать десериализуемую дату-время.
            Избавляет от необходимости проверять Kind.


            А использовать DateTime.Now на сервере в принципе плохая идея, если только для служебных данных.


            1. ETman
              04.04.2017 14:27

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


  1. forahrr
    03.04.2017 13:25

    Мы использовали очень похожий подход. Спасибо за статью


  1. Bonart
    04.04.2017 12:04

    Жесткий vendor lock, обратная несовместимость, неполнота локальной информации, а реальных преимуществ нет.
    Пример с опозданием на работу в любом случае требует привязки к конкретному офису и его расположению — в эту информацию таймзона обязательно входит.
    Пока полученный результат больше всего похож на велосипед с квадратными колесами.
    Что только люди ни придумают лишь бы UTC не хранить.


    1. qw1
      04.04.2017 12:20

      Пример с опозданием на работу в любом случае требует привязки к конкретному офису и его расположению — в эту информацию таймзона обязательно входит.
      Однако, таймзона в Усть-Урюпинске сейчас и тайм-зона там же 2 года назад — совершенно разные вещи ))) И нормального надёжного способа получить её нет, разве что парсить местные газеты с местными указами.


      1. Bonart
        04.04.2017 12:28
        +1

        Распорядок дня с привязкой к конкретной организации, должности, смене тоже сам собой не нарисуется. Зону за прошлое получить не проще и не сложнее. А вот что в смещении после указов Медведева были ошибки и много — 99%. Таймзону уточнить и исправить достаточно однажды, а вот со смещениями придется повозиться.
        Даже в идеальном для авторов идеи варианте получается не айс.


      1. flancer
        04.04.2017 12:45
        +1

        Есть велосипеды со специальными колесами для езды по ступенькам и на них действительно удобно ездить по ступенькам.


        image


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


        1. ggrnd0
          04.04.2017 13:06
          +1

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


          1. flancer
            04.04.2017 13:14
            +1

            Реальные велосипеды реального мира, если что:
            image


    1. ETman
      04.04.2017 14:02

      Что только люди ни придумают лишь бы UTC не хранить.

      DateTimeOffset эквивалентно хранению UTC. При условии, что используется базовое смещение от UTC.


      1. Bonart
        04.04.2017 15:44

        Только зачем он нужен?
        Усложнение, ломающее изменение, избыточная информация, отсутствие поддержки от большинства СУБД.
        И все это ради вариантов использования, которые все равно требуют дополнительной информации о месте события, в которое все равно входит таймзона.
        Про время же события необходимо и достаточно хранить UTC.
        Получается простое, чистое и элегантное сочетание преждевременной оптимизации с денормализацией.


        1. qw1
          04.04.2017 19:23
          +1

          Если сразу делать в UTC, это можно.
          А вот для миграции (причём на лету, без остановки) системы с длинной историей развития несовместимые типы очень кстати — видно, где уже поправили, а где ещё нет.


          1. Bonart
            05.04.2017 10:34

            Если специально нужен несовместимый тип то в нем надо хранить UTC без всякой дополнительной мишуры.
            DateTimeOffset — удобная вещь для расчетов, но не для хранения.